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