1from __future__ import absolute_import, division, print_function
2
3import codecs
4import os
5import platform
6import re
7import sys
8
9from distutils.command.build import build
10from distutils.command.build_clib import build_clib
11from distutils.errors import DistutilsSetupError
12
13from setuptools import find_packages, setup
14from setuptools.command.install import install
15
16
17###############################################################################
18
19NAME = "argon2_cffi"
20PACKAGES = find_packages(where="src")
21
22# Optimized version requires SSE2 extensions.  They have been around since
23# 2001 so we try to compile it on every recent-ish x86.
24optimized = platform.machine() in ("i686", "x86", "x86_64", "AMD64")
25
26CFFI_MODULES = ["src/argon2/_ffi_build.py:ffi"]
27lib_base = os.path.join("extras", "libargon2", "src")
28include_dirs = [
29    os.path.join(lib_base, "..", "include"),
30    os.path.join(lib_base, "blake2"),
31]
32sources = [
33    os.path.join(lib_base, path)
34    for path in [
35        "argon2.c",
36        os.path.join("blake2", "blake2b.c"),
37        "core.c",
38        "encoding.c",
39        "opt.c" if optimized else "ref.c",
40        "thread.c",
41    ]
42]
43
44# Add vendored integer types headers if necessary.
45windows = "win32" in str(sys.platform).lower()
46if windows:
47    int_base = "extras/msinttypes/"
48    inttypes = int_base + "inttypes"
49    stdint = int_base + "stdint"
50    vi = sys.version_info[0:2]
51    if vi in [(2, 6), (2, 7)]:
52        # VS 2008 needs both.
53        include_dirs += [inttypes, stdint]
54    elif vi in [(3, 3), (3, 4)]:
55        # VS 2010 needs inttypes.h and fails with both.
56        include_dirs += [inttypes]
57
58LIBRARIES = [("argon2", {"include_dirs": include_dirs, "sources": sources})]
59META_PATH = os.path.join("src", "argon2", "__init__.py")
60KEYWORDS = ["password", "hash", "hashing", "security"]
61PROJECT_URLS = {
62    "Documentation": "https://argon2-cffi.readthedocs.io/",
63    "Bug Tracker": "https://github.com/hynek/argon2_cffi/issues",
64    "Source Code": "https://github.com/hynek/argon2_cffi",
65}
66CLASSIFIERS = [
67    "Development Status :: 5 - Production/Stable",
68    "Intended Audience :: Developers",
69    "License :: OSI Approved :: MIT License",
70    "Natural Language :: English",
71    "Operating System :: MacOS :: MacOS X",
72    "Operating System :: Microsoft :: Windows",
73    "Operating System :: POSIX",
74    "Operating System :: Unix",
75    "Programming Language :: Python :: 2",
76    "Programming Language :: Python :: 2.7",
77    "Programming Language :: Python :: 3",
78    "Programming Language :: Python :: 3.4",
79    "Programming Language :: Python :: 3.5",
80    "Programming Language :: Python :: 3.6",
81    "Programming Language :: Python :: 3.7",
82    "Programming Language :: Python :: Implementation :: CPython",
83    "Programming Language :: Python :: Implementation :: PyPy",
84    "Programming Language :: Python",
85    "Topic :: Security :: Cryptography",
86    "Topic :: Security",
87    "Topic :: Software Development :: Libraries :: Python Modules",
88]
89
90SETUP_REQUIRES = ["cffi"]
91if windows and sys.version_info[0] == 2:
92    # required for "Microsoft Visual C++ Compiler for Python 2.7"
93    # https://www.microsoft.com/en-us/download/details.aspx?id=44266
94    SETUP_REQUIRES.append("setuptools>=6.0")
95
96INSTALL_REQUIRES = ["cffi>=1.0.0", "six"]
97# We're not building an universal wheel so this works.
98if sys.version_info[0:2] < (3, 4):
99    INSTALL_REQUIRES += ["enum34"]
100
101EXTRAS_REQUIRE = {
102    "docs": ["sphinx"],
103    "tests": ["coverage", "hypothesis", "pytest"],
104}
105EXTRAS_REQUIRE["dev"] = (
106    EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] + ["wheel", "pre-commit"]
107)
108
109###############################################################################
110
111
112def keywords_with_side_effects(argv):
113    """
114    Get a dictionary with setup keywords that (can) have side effects.
115
116    :param argv: A list of strings with command line arguments.
117
118    :returns: A dictionary with keyword arguments for the ``setup()`` function.
119        This setup.py script uses the setuptools 'setup_requires' feature
120        because this is required by the cffi package to compile extension
121        modules. The purpose of ``keywords_with_side_effects()`` is to avoid
122        triggering the cffi build process as a result of setup.py invocations
123        that don't need the cffi module to be built (setup.py serves the dual
124        purpose of exposing package metadata).
125
126    Stolen from pyca/cryptography.
127    """
128    no_setup_requires_arguments = (
129        "-h",
130        "--help",
131        "-n",
132        "--dry-run",
133        "-q",
134        "--quiet",
135        "-v",
136        "--verbose",
137        "-V",
138        "--version",
139        "--author",
140        "--author-email",
141        "--classifiers",
142        "--contact",
143        "--contact-email",
144        "--description",
145        "--egg-base",
146        "--fullname",
147        "--help-commands",
148        "--keywords",
149        "--licence",
150        "--license",
151        "--long-description",
152        "--maintainer",
153        "--maintainer-email",
154        "--name",
155        "--no-user-cfg",
156        "--obsoletes",
157        "--platforms",
158        "--provides",
159        "--requires",
160        "--url",
161        "clean",
162        "egg_info",
163        "register",
164        "sdist",
165        "upload",
166    )
167
168    def is_short_option(argument):
169        """Check whether a command line argument is a short option."""
170        return len(argument) >= 2 and argument[0] == "-" and argument[1] != "-"
171
172    def expand_short_options(argument):
173        """Expand combined short options into canonical short options."""
174        return ("-" + char for char in argument[1:])
175
176    def argument_without_setup_requirements(argv, i):
177        """Check whether a command line argument needs setup requirements."""
178        if argv[i] in no_setup_requires_arguments:
179            # Simple case: An argument which is either an option or a command
180            # which doesn't need setup requirements.
181            return True
182        elif is_short_option(argv[i]) and all(
183            option in no_setup_requires_arguments
184            for option in expand_short_options(argv[i])
185        ):
186            # Not so simple case: Combined short options none of which need
187            # setup requirements.
188            return True
189        elif argv[i - 1 : i] == ["--egg-base"]:
190            # Tricky case: --egg-info takes an argument which should not make
191            # us use setup_requires (defeating the purpose of this code).
192            return True
193        else:
194            return False
195
196    if all(
197        argument_without_setup_requirements(argv, i)
198        for i in range(1, len(argv))
199    ):
200        return {"cmdclass": {"build": DummyBuild, "install": DummyInstall}}
201    else:
202        use_system_argon2 = (
203            os.environ.get("ARGON2_CFFI_USE_SYSTEM", "0") == "1"
204        )
205        if use_system_argon2:
206            disable_subcommand(build, "build_clib")
207        return {
208            "setup_requires": SETUP_REQUIRES,
209            "cffi_modules": CFFI_MODULES,
210            "libraries": LIBRARIES,
211            "cmdclass": {"build_clib": BuildCLibWithCompilerFlags},
212        }
213
214
215def disable_subcommand(command, subcommand_name):
216    for name, method in command.sub_commands:
217        if name == subcommand_name:
218            command.sub_commands.remove((subcommand_name, method))
219            break
220
221
222setup_requires_error = (
223    "Requested setup command that needs 'setup_requires' while command line "
224    "arguments implied a side effect free command or option."
225)
226
227
228class DummyBuild(build):
229    """
230    This class makes it very obvious when ``keywords_with_side_effects()`` has
231    incorrectly interpreted the command line arguments to ``setup.py build`` as
232    one of the 'side effect free' commands or options.
233    """
234
235    def run(self):
236        raise RuntimeError(setup_requires_error)
237
238
239class DummyInstall(install):
240    """
241    This class makes it very obvious when ``keywords_with_side_effects()`` has
242    incorrectly interpreted the command line arguments to ``setup.py install``
243    as one of the 'side effect free' commands or options.
244    """
245
246    def run(self):
247        raise RuntimeError(setup_requires_error)
248
249
250HERE = os.path.abspath(os.path.dirname(__file__))
251
252
253def read(*parts):
254    """
255    Build an absolute path from *parts* and and return the contents of the
256    resulting file.  Assume UTF-8 encoding.
257    """
258    with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f:
259        return f.read()
260
261
262META_FILE = read(META_PATH)
263
264
265def find_meta(meta):
266    """
267    Extract __*meta*__ from META_FILE.
268    """
269    meta_match = re.search(
270        r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), META_FILE, re.M
271    )
272    if meta_match:
273        return meta_match.group(1)
274    raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta))
275
276
277VERSION = find_meta("version")
278URL = find_meta("url")
279LONG = (
280    read("README.rst")
281    + "\n\n"
282    + "Release Information\n"
283    + "===================\n\n"
284    + re.search(
285        r"(\d+.\d.\d \(.*?\)\n.*?)\n\n\n----\n\n\n",
286        read("CHANGELOG.rst"),
287        re.S,
288    ).group(1)
289    + "\n\n`Full changelog "
290    + "<{url}en/stable/changelog.html>`_.\n\n".format(url=URL)
291    + read("AUTHORS.rst")
292)
293
294
295class BuildCLibWithCompilerFlags(build_clib):
296    """
297    We need to pass ``-msse2`` for the optimized build.
298    """
299
300    def build_libraries(self, libraries):
301        """
302        Mostly copy pasta from ``distutils.command.build_clib``.
303        """
304        for (lib_name, build_info) in libraries:
305            sources = build_info.get("sources")
306            if sources is None or not isinstance(sources, (list, tuple)):
307                raise DistutilsSetupError(
308                    "in 'libraries' option (library '%s'), "
309                    "'sources' must be present and must be "
310                    "a list of source filenames" % lib_name
311                )
312            sources = list(sources)
313
314            print("building '%s' library" % (lib_name,))
315
316            # First, compile the source code to object files in the library
317            # directory.  (This should probably change to putting object
318            # files in a temporary build directory.)
319            macros = build_info.get("macros")
320            include_dirs = build_info.get("include_dirs")
321            objects = self.compiler.compile(
322                sources,
323                extra_preargs=["-msse2"] if optimized and not windows else [],
324                output_dir=self.build_temp,
325                macros=macros,
326                include_dirs=include_dirs,
327                debug=self.debug,
328            )
329
330            # Now "link" the object files together into a static library.
331            # (On Unix at least, this isn't really linking -- it just
332            # builds an archive.  Whatever.)
333            self.compiler.create_static_lib(
334                objects, lib_name, output_dir=self.build_clib, debug=self.debug
335            )
336
337
338if __name__ == "__main__":
339    setup(
340        name=NAME,
341        description=find_meta("description"),
342        license=find_meta("license"),
343        url=URL,
344        project_urls=PROJECT_URLS,
345        version=VERSION,
346        author=find_meta("author"),
347        author_email=find_meta("email"),
348        maintainer=find_meta("author"),
349        maintainer_email=find_meta("email"),
350        long_description=LONG,
351        keywords=KEYWORDS,
352        packages=PACKAGES,
353        package_dir={"": "src"},
354        classifiers=CLASSIFIERS,
355        install_requires=INSTALL_REQUIRES,
356        extras_require=EXTRAS_REQUIRE,
357        # CFFI
358        zip_safe=False,
359        ext_package="argon2",
360        **keywords_with_side_effects(sys.argv)
361    )
362