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