1"""update version string during build""" 2#============================================================================= 3# imports 4#============================================================================= 5from __future__ import absolute_import, division, print_function 6# core 7import datetime 8from distutils.dist import Distribution 9import os 10import re 11import subprocess 12import time 13# pkg 14# local 15__all__ = [ 16 "stamp_source", 17 "stamp_distutils_output", 18 "append_hg_revision", 19 "as_bool", 20] 21#============================================================================= 22# helpers 23#============================================================================= 24def get_command_class(opts, name): 25 return opts['cmdclass'].get(name) or Distribution().get_command_class(name) 26 27def get_command_options(opts, command): 28 return opts.setdefault("options", {}).setdefault(command, {}) 29 30def set_command_options(opts, command, **kwds): 31 get_command_options(opts, command).update(kwds) 32 33def _get_file(path): 34 with open(path, "r") as fh: 35 return fh.read() 36 37 38def _replace_file(path, content, dry_run=False): 39 if dry_run: 40 return 41 if os.path.exists(path): 42 # sdist likes to use hardlinks, have to remove them first, 43 # or we modify *source* file 44 os.unlink(path) 45 with open(path, "w") as fh: 46 fh.write(content) 47 48 49def stamp_source(base_dir, version, dry_run=False): 50 """ 51 update version info in passlib source 52 """ 53 # 54 # update version string in toplevel package source 55 # 56 path = os.path.join(base_dir, "passlib", "__init__.py") 57 content = _get_file(path) 58 content, count = re.subn('(?m)^__version__\s*=.*$', 59 '__version__ = ' + repr(version), 60 content) 61 assert count == 1, "failed to replace version string" 62 _replace_file(path, content, dry_run=dry_run) 63 64 # 65 # update flag in setup.py 66 # (not present when called from bdist_wheel, etc) 67 # 68 path = os.path.join(base_dir, "setup.py") 69 if os.path.exists(path): 70 content = _get_file(path) 71 content, count = re.subn('(?m)^stamp_build\s*=.*$', 72 'stamp_build = False', content) 73 assert count == 1, "failed to update 'stamp_build' flag" 74 _replace_file(path, content, dry_run=dry_run) 75 76 77def stamp_distutils_output(opts, version): 78 79 # subclass buildpy to update version string in source 80 _build_py = get_command_class(opts, "build_py") 81 class build_py(_build_py): 82 def build_packages(self): 83 _build_py.build_packages(self) 84 stamp_source(self.build_lib, version, self.dry_run) 85 opts['cmdclass']['build_py'] = build_py 86 87 # subclass sdist to do same thing 88 _sdist = get_command_class(opts, "sdist") 89 class sdist(_sdist): 90 def make_release_tree(self, base_dir, files): 91 _sdist.make_release_tree(self, base_dir, files) 92 stamp_source(base_dir, version, self.dry_run) 93 opts['cmdclass']['sdist'] = sdist 94 95 96def as_bool(value): 97 return (value or "").lower() in "yes y true t 1".split() 98 99 100def append_hg_revision(version): 101 102 # call HG via subprocess 103 # NOTE: for py26 compat, using Popen() instead of check_output() 104 try: 105 proc = subprocess.Popen(["hg", "tip", "--template", "{date(date, '%Y%m%d%H%M%S')}+hg.{node|short}"], 106 stdout=subprocess.PIPE) 107 stamp, _ = proc.communicate() 108 if proc.returncode: 109 raise subprocess.CalledProcessError(1, []) 110 stamp = stamp.decode("ascii") 111 except (OSError, subprocess.CalledProcessError): 112 # fallback - just use build date 113 now = int(os.environ.get('SOURCE_DATE_EPOCH') or time.time()) 114 build_date = datetime.datetime.utcfromtimestamp(now) 115 stamp = build_date.strftime("%Y%m%d%H%M%S") 116 117 # modify version 118 if version.endswith((".dev0", ".post0")): 119 version = version[:-1] + stamp 120 else: 121 version += ".post" + stamp 122 123 return version 124 125def install_build_py_exclude(opts): 126 127 _build_py = get_command_class(opts, "build_py") 128 129 class build_py(_build_py): 130 131 user_options = _build_py.user_options + [ 132 ("exclude-packages=", None, 133 "exclude packages from builds"), 134 ] 135 136 exclude_packages = None 137 138 def finalize_options(self): 139 _build_py.finalize_options(self) 140 target = self.packages 141 for package in self.exclude_packages or []: 142 if package in target: 143 target.remove(package) 144 145 opts['cmdclass']['build_py'] = build_py 146 147#============================================================================= 148# eof 149#============================================================================= 150