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