blob: 469d79805b85650946e431fb81d58e0fd4298704 [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
9from setuptools import setup, find_packages, Command
10from distutils import log
11from distutils.util import convert_path
12import subprocess as sp
13import contextlib
14import re
jvr3285b4b2001-08-09 18:47:22 +000015
Haibo Huang8b3c57b2018-07-03 17:43:11 -070016# Force distutils to use py_compile.compile() function with 'doraise' argument
17# set to True, in order to raise an exception on compilation errors
18import py_compile
19orig_py_compile = py_compile.compile
jvr3285b4b2001-08-09 18:47:22 +000020
Haibo Huang8b3c57b2018-07-03 17:43:11 -070021def doraise_py_compile(file, cfile=None, dfile=None, doraise=False):
22 orig_py_compile(file, cfile=cfile, dfile=dfile, doraise=True)
23
24py_compile.compile = doraise_py_compile
25
Haibo Huang8b3c57b2018-07-03 17:43:11 -070026needs_wheel = {'bdist_wheel'}.intersection(sys.argv)
27wheel = ['wheel'] if needs_wheel else []
28needs_bumpversion = {'release'}.intersection(sys.argv)
29bumpversion = ['bump2version'] if needs_bumpversion else []
30
Elliott Hughes69c9aca2018-10-30 14:11:58 -070031extras_require = {
32 # for fontTools.ufoLib: to read/write UFO fonts
33 "ufo": [
Haibo Huangf08648c2019-02-01 17:02:23 -080034 "fs >= 2.2.0, < 3",
Elliott Hughes69c9aca2018-10-30 14:11:58 -070035 ],
36 # for fontTools.misc.etree and fontTools.misc.plistlib: use lxml to
37 # read/write XML files (faster/safer than built-in ElementTree)
38 "lxml": [
39 "lxml >= 4.0, < 5",
Elliott Hughes69c9aca2018-10-30 14:11:58 -070040 ],
41 # for fontTools.sfnt and fontTools.woff2: to compress/uncompress
42 # WOFF 1.0 and WOFF 2.0 webfonts.
43 "woff": [
44 "brotli >= 1.0.1; platform_python_implementation != 'PyPy'",
45 "brotlipy >= 0.7.0; platform_python_implementation == 'PyPy'",
46 "zopfli >= 0.1.4",
47 ],
48 # for fontTools.unicode and fontTools.unicodedata: to use the latest version
49 # of the Unicode Character Database instead of the built-in unicodedata
50 # which varies between python versions and may be outdated.
51 "unicode": [
52 # the unicodedata2 extension module doesn't work on PyPy.
Haibo Huang5406a6a2020-02-26 16:36:20 -080053 # Python 3.8 already has Unicode 12.1, so the backport is not needed.
Elliott Hughes69c9aca2018-10-30 14:11:58 -070054 (
Haibo Huang5406a6a2020-02-26 16:36:20 -080055 "unicodedata2 >= 12.1.0; "
Haibo Huang4c8220a2019-05-31 16:12:23 -070056 "python_version < '3.8' and platform_python_implementation != 'PyPy'"
Elliott Hughes69c9aca2018-10-30 14:11:58 -070057 ),
58 ],
Haibo Huang79019a02019-01-08 14:14:22 -080059 # for graphite type tables in ttLib/tables (Silf, Glat, Gloc)
60 "graphite": [
61 "lz4 >= 1.7.4.2"
62 ],
Elliott Hughes69c9aca2018-10-30 14:11:58 -070063 # for fontTools.interpolatable: to solve the "minimum weight perfect
64 # matching problem in bipartite graphs" (aka Assignment problem)
65 "interpolatable": [
66 # use pure-python alternative on pypy
67 "scipy; platform_python_implementation != 'PyPy'",
68 "munkres; platform_python_implementation == 'PyPy'",
69 ],
Haibo Huang79019a02019-01-08 14:14:22 -080070 # for fontTools.varLib.plot, to visualize DesignSpaceDocument and resulting
71 # VariationModel
72 "plot": [
73 # TODO: figure out the minimum version of matplotlib that we need
74 "matplotlib",
75 ],
Elliott Hughes69c9aca2018-10-30 14:11:58 -070076 # for fontTools.misc.symfont, module for symbolic font statistics analysis
77 "symfont": [
78 "sympy",
79 ],
80 # To get file creator and type of Macintosh PostScript Type 1 fonts (macOS only)
81 "type1": [
82 "xattr; sys_platform == 'darwin'",
83 ],
84}
85# use a special 'all' key as shorthand to includes all the extra dependencies
86extras_require["all"] = sum(extras_require.values(), [])
87
88
Haibo Huang8b3c57b2018-07-03 17:43:11 -070089# Trove classifiers for PyPI
90classifiers = {"classifiers": [
91 "Development Status :: 5 - Production/Stable",
92 "Environment :: Console",
93 "Environment :: Other Environment",
94 "Intended Audience :: Developers",
95 "Intended Audience :: End Users/Desktop",
96 "License :: OSI Approved :: MIT License",
97 "Natural Language :: English",
98 "Operating System :: OS Independent",
99 "Programming Language :: Python",
100 "Programming Language :: Python :: 2",
101 "Programming Language :: Python :: 3",
102 "Topic :: Text Processing :: Fonts",
103 "Topic :: Multimedia :: Graphics",
104 "Topic :: Multimedia :: Graphics :: Graphics Conversion",
105]}
jvr5808f3f2001-08-09 23:03:47 +0000106
107
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700108# concatenate README.rst and NEWS.rest into long_description so they are
109# displayed on the FontTols project page on PyPI
110with io.open("README.rst", "r", encoding="utf-8") as readme:
111 long_description = readme.read()
112long_description += "\nChangelog\n~~~~~~~~~\n\n"
113with io.open("NEWS.rst", "r", encoding="utf-8") as changelog:
114 long_description += changelog.read()
jvr059cbe32002-07-01 09:11:01 +0000115
116
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700117@contextlib.contextmanager
118def capture_logger(name):
119 """ Context manager to capture a logger output with a StringIO stream.
120 """
121 import logging
jvr91bde172003-01-03 21:01:07 +0000122
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700123 logger = logging.getLogger(name)
124 try:
125 import StringIO
126 stream = StringIO.StringIO()
127 except ImportError:
128 stream = io.StringIO()
129 handler = logging.StreamHandler(stream)
130 logger.addHandler(handler)
131 try:
132 yield stream
133 finally:
134 logger.removeHandler(handler)
135
136
137class release(Command):
138 """
139 Tag a new release with a single command, using the 'bumpversion' tool
140 to update all the version strings in the source code.
141 The version scheme conforms to 'SemVer' and PEP 440 specifications.
142
143 Firstly, the pre-release '.devN' suffix is dropped to signal that this is
144 a stable release. If '--major' or '--minor' options are passed, the
145 the first or second 'semver' digit is also incremented. Major is usually
146 for backward-incompatible API changes, while minor is used when adding
147 new backward-compatible functionalities. No options imply 'patch' or bug-fix
148 release.
149
150 A new header is also added to the changelog file ("NEWS.rst"), containing
151 the new version string and the current 'YYYY-MM-DD' date.
152
153 All changes are committed, and an annotated git tag is generated. With the
154 --sign option, the tag is GPG-signed with the user's default key.
155
156 Finally, the 'patch' part of the version string is bumped again, and a
157 pre-release suffix '.dev0' is appended to mark the opening of a new
158 development cycle.
159
160 Links:
161 - http://semver.org/
162 - https://www.python.org/dev/peps/pep-0440/
163 - https://github.com/c4urself/bump2version
164 """
165
166 description = "update version strings for release"
167
168 user_options = [
169 ("major", None, "bump the first digit (incompatible API changes)"),
170 ("minor", None, "bump the second digit (new backward-compatible features)"),
171 ("sign", "s", "make a GPG-signed tag, using the default key"),
172 ("allow-dirty", None, "don't abort if working directory is dirty"),
173 ]
174
175 changelog_name = "NEWS.rst"
176 version_RE = re.compile("^[0-9]+\.[0-9]+")
177 date_fmt = u"%Y-%m-%d"
178 header_fmt = u"%s (released %s)"
179 commit_message = "Release {new_version}"
180 tag_name = "{new_version}"
181 version_files = [
182 "setup.cfg",
183 "setup.py",
184 "Lib/fontTools/__init__.py",
185 ]
186
187 def initialize_options(self):
188 self.minor = False
189 self.major = False
190 self.sign = False
191 self.allow_dirty = False
192
193 def finalize_options(self):
194 if all([self.major, self.minor]):
195 from distutils.errors import DistutilsOptionError
196 raise DistutilsOptionError("--major/--minor are mutually exclusive")
197 self.part = "major" if self.major else "minor" if self.minor else None
198
199 def run(self):
200 if self.part is not None:
201 log.info("bumping '%s' version" % self.part)
202 self.bumpversion(self.part, commit=False)
203 release_version = self.bumpversion(
204 "release", commit=False, allow_dirty=True)
205 else:
206 log.info("stripping pre-release suffix")
207 release_version = self.bumpversion("release")
208 log.info(" version = %s" % release_version)
209
210 changes = self.format_changelog(release_version)
211
212 self.git_commit(release_version)
213 self.git_tag(release_version, changes, self.sign)
214
215 log.info("bumping 'patch' version and pre-release suffix")
216 next_dev_version = self.bumpversion('patch', commit=True)
217 log.info(" version = %s" % next_dev_version)
218
219 def git_commit(self, version):
220 """ Stage and commit all relevant version files, and format the commit
221 message with specified 'version' string.
222 """
223 files = self.version_files + [self.changelog_name]
224
225 log.info("committing changes")
226 for f in files:
227 log.info(" %s" % f)
228 if self.dry_run:
229 return
230 sp.check_call(["git", "add"] + files)
231 msg = self.commit_message.format(new_version=version)
232 sp.check_call(["git", "commit", "-m", msg], stdout=sp.PIPE)
233
234 def git_tag(self, version, message, sign=False):
235 """ Create annotated git tag with given 'version' and 'message'.
236 Optionally 'sign' the tag with the user's GPG key.
237 """
238 log.info("creating %s git tag '%s'" % (
239 "signed" if sign else "annotated", version))
240 if self.dry_run:
241 return
242 # create an annotated (or signed) tag from the new version
243 tag_opt = "-s" if sign else "-a"
244 tag_name = self.tag_name.format(new_version=version)
245 proc = sp.Popen(
246 ["git", "tag", tag_opt, "-F", "-", tag_name], stdin=sp.PIPE)
247 # use the latest changes from the changelog file as the tag message
248 tag_message = u"%s\n\n%s" % (tag_name, message)
249 proc.communicate(tag_message.encode('utf-8'))
250 if proc.returncode != 0:
251 sys.exit(proc.returncode)
252
253 def bumpversion(self, part, commit=False, message=None, allow_dirty=None):
254 """ Run bumpversion.main() with the specified arguments, and return the
255 new computed version string (cf. 'bumpversion --help' for more info)
256 """
Haibo Huang5406a6a2020-02-26 16:36:20 -0800257 import bumpversion.cli
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700258
259 args = (
260 (['--verbose'] if self.verbose > 1 else []) +
261 (['--dry-run'] if self.dry_run else []) +
262 (['--allow-dirty'] if (allow_dirty or self.allow_dirty) else []) +
263 (['--commit'] if commit else ['--no-commit']) +
264 (['--message', message] if message is not None else []) +
265 ['--list', part]
266 )
267 log.debug("$ bumpversion %s" % " ".join(a.replace(" ", "\\ ") for a in args))
268
269 with capture_logger("bumpversion.list") as out:
Haibo Huang5406a6a2020-02-26 16:36:20 -0800270 bumpversion.cli.main(args)
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700271
272 last_line = out.getvalue().splitlines()[-1]
273 new_version = last_line.replace("new_version=", "")
274 return new_version
275
276 def format_changelog(self, version):
277 """ Write new header at beginning of changelog file with the specified
278 'version' and the current date.
279 Return the changelog content for the current release.
280 """
281 from datetime import datetime
282
283 log.info("formatting changelog")
284
285 changes = []
286 with io.open(self.changelog_name, "r+", encoding="utf-8") as f:
287 for ln in f:
288 if self.version_RE.match(ln):
289 break
290 else:
291 changes.append(ln)
292 if not self.dry_run:
293 f.seek(0)
294 content = f.read()
295 date = datetime.today().strftime(self.date_fmt)
296 f.seek(0)
297 header = self.header_fmt % (version, date)
298 f.write(header + u"\n" + u"-"*len(header) + u"\n\n" + content)
299
300 return u"".join(changes)
301
302
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700303def find_data_files(manpath="share/man"):
304 """ Find FontTools's data_files (just man pages at this point).
305
306 By default, we install man pages to "share/man" directory relative to the
307 base installation directory for data_files. The latter can be changed with
308 the --install-data option of 'setup.py install' sub-command.
309
310 E.g., if the data files installation directory is "/usr", the default man
311 page installation directory will be "/usr/share/man".
312
313 You can override this via the $FONTTOOLS_MANPATH environment variable.
314
315 E.g., on some BSD systems man pages are installed to 'man' instead of
316 'share/man'; you can export $FONTTOOLS_MANPATH variable just before
317 installing:
318
319 $ FONTTOOLS_MANPATH="man" pip install -v .
320 [...]
321 running install_data
322 copying Doc/man/ttx.1 -> /usr/man/man1
323
324 When installing from PyPI, for this variable to have effect you need to
325 force pip to install from the source distribution instead of the wheel
326 package (otherwise setup.py is not run), by using the --no-binary option:
327
328 $ FONTTOOLS_MANPATH="man" pip install --no-binary=fonttools fonttools
329
330 Note that you can only override the base man path, i.e. without the
331 section number (man1, man3, etc.). The latter is always implied to be 1,
332 for "general commands".
333 """
334
335 # get base installation directory for man pages
336 manpagebase = os.environ.get('FONTTOOLS_MANPATH', convert_path(manpath))
337 # all our man pages go to section 1
338 manpagedir = pjoin(manpagebase, 'man1')
339
340 manpages = [f for f in glob(pjoin('Doc', 'man', 'man1', '*.1')) if isfile(f)]
341
342 data_files = [(manpagedir, manpages)]
343 return data_files
344
jvr91bde172003-01-03 21:01:07 +0000345
jvrfdf2d772002-05-03 18:57:02 +0000346setup(
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700347 name="fonttools",
Haibo Huang5406a6a2020-02-26 16:36:20 -0800348 version="4.4.1",
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700349 description="Tools to manipulate font files",
350 author="Just van Rossum",
351 author_email="just@letterror.com",
352 maintainer="Behdad Esfahbod",
353 maintainer_email="behdad@behdad.org",
354 url="http://github.com/fonttools/fonttools",
355 license="MIT",
356 platforms=["Any"],
Haibo Huang5406a6a2020-02-26 16:36:20 -0800357 python_requires=">=3.6",
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700358 long_description=long_description,
359 package_dir={'': 'Lib'},
360 packages=find_packages("Lib"),
361 include_package_data=True,
362 data_files=find_data_files(),
Elliott Hughes69c9aca2018-10-30 14:11:58 -0700363 setup_requires=wheel + bumpversion,
364 extras_require=extras_require,
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700365 entry_points={
366 'console_scripts': [
367 "fonttools = fontTools.__main__:main",
368 "ttx = fontTools.ttx:main",
369 "pyftsubset = fontTools.subset:main",
370 "pyftmerge = fontTools.merge:main",
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700371 ]
372 },
373 cmdclass={
374 "release": release,
Haibo Huang8b3c57b2018-07-03 17:43:11 -0700375 },
376 **classifiers
377)