1#!/usr/local/bin/python3.8
2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
3
4
5__license__   = 'GPL v3'
6__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
7__docformat__ = 'restructuredtext en'
8
9import errno
10import os
11import platform
12import re
13import shutil
14import subprocess
15import sys
16import tempfile
17import time
18import hashlib
19from contextlib import contextmanager
20from functools import lru_cache
21
22is64bit = platform.architecture()[0] == '64bit'
23iswindows = re.search('win(32|64)', sys.platform)
24ismacos = 'darwin' in sys.platform
25isfreebsd = 'freebsd' in sys.platform
26isnetbsd = 'netbsd' in sys.platform
27isdragonflybsd = 'dragonfly' in sys.platform
28isbsd = isnetbsd or isfreebsd or isdragonflybsd
29ishaiku = 'haiku1' in sys.platform
30islinux = not ismacos and not iswindows and not isbsd and not ishaiku
31sys.setup_dir = os.path.dirname(os.path.abspath(__file__))
32SRC = os.path.abspath(os.path.join(os.path.dirname(sys.setup_dir), 'src'))
33sys.path.insert(0, SRC)
34sys.resources_location = os.path.join(os.path.dirname(SRC), 'resources')
35sys.extensions_location = os.path.abspath(os.environ.get('CALIBRE_SETUP_EXTENSIONS_PATH', os.path.join(SRC, 'calibre', 'plugins')))
36sys.running_from_setup = True
37
38__version__ = __appname__ = modules = functions = basenames = scripts = None
39
40_cache_dir_built = False
41
42
43def newer(targets, sources):
44    if hasattr(targets, 'rjust'):
45        targets = [targets]
46    if hasattr(sources, 'rjust'):
47        sources = [sources]
48    for f in targets:
49        if not os.path.exists(f):
50            return True
51    ttimes = map(lambda x: os.stat(x).st_mtime, targets)
52    stimes = map(lambda x: os.stat(x).st_mtime, sources)
53    newest_source, oldest_target = max(stimes), min(ttimes)
54    return newest_source > oldest_target
55
56
57def dump_json(obj, path, indent=4):
58    import json
59    with open(path, 'wb') as f:
60        data = json.dumps(obj, indent=indent)
61        if not isinstance(data, bytes):
62            data = data.encode('utf-8')
63        f.write(data)
64
65
66@lru_cache
67def curl_supports_etags():
68    return '--etag-compare' in subprocess.check_output(['curl', '--help', 'all']).decode('utf-8')
69
70
71def download_securely(url):
72    # We use curl here as on some OSes (OS X) when bootstrapping calibre,
73    # python will be unable to validate certificates until after cacerts is
74    # installed
75    if not curl_supports_etags():
76        return subprocess.check_output(['curl', '-fsSL', url])
77    url_hash = hashlib.sha1(url.encode('utf-8')).hexdigest()
78    cache_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.cache', 'download', url_hash)
79    os.makedirs(cache_dir, exist_ok=True)
80    subprocess.check_call(
81        ['curl', '-fsSL', '--etag-compare', 'etag.txt', '--etag-save', 'etag.txt', '-o', 'data.bin', url],
82        cwd=cache_dir
83    )
84    with open(os.path.join(cache_dir, 'data.bin'), 'rb') as f:
85        return f.read()
86
87
88def build_cache_dir():
89    global _cache_dir_built
90    ans = os.path.join(os.path.dirname(SRC), '.build-cache')
91    if not _cache_dir_built:
92        _cache_dir_built = True
93        try:
94            os.mkdir(ans)
95        except EnvironmentError as err:
96            if err.errno != errno.EEXIST:
97                raise
98    return ans
99
100
101def require_git_master(branch='master'):
102    if subprocess.check_output(['git', 'symbolic-ref', '--short', 'HEAD']).decode('utf-8').strip() != branch:
103        raise SystemExit('You must be in the {} git branch'.format(branch))
104
105
106def require_clean_git():
107    c = subprocess.check_call
108    p = subprocess.Popen
109    c('git rev-parse --verify HEAD'.split(), stdout=subprocess.DEVNULL)
110    c('git update-index -q --ignore-submodules --refresh'.split())
111    if p('git diff-files --quiet --ignore-submodules'.split()).wait() != 0:
112        raise SystemExit('You have unstaged changes in your working tree')
113    if p('git diff-index --cached --quiet --ignore-submodules HEAD --'.split()).wait() != 0:
114        raise SystemExit('Your git index contains uncommitted changes')
115
116
117def initialize_constants():
118    global __version__, __appname__, modules, functions, basenames, scripts
119
120    with open(os.path.join(SRC, 'calibre/constants.py'), 'rb') as f:
121        src = f.read().decode('utf-8')
122    nv = re.search(r'numeric_version\s+=\s+\((\d+), (\d+), (\d+)\)', src)
123    __version__ = '%s.%s.%s'%(nv.group(1), nv.group(2), nv.group(3))
124    __appname__ = re.search(r'__appname__\s+=\s+(u{0,1})[\'"]([^\'"]+)[\'"]',
125            src).group(2)
126    with open(os.path.join(SRC, 'calibre/linux.py'), 'rb') as sf:
127        epsrc = re.compile(r'entry_points = (\{.*?\})', re.DOTALL).\
128                search(sf.read().decode('utf-8')).group(1)
129    entry_points = eval(epsrc, {'__appname__': __appname__})
130
131    def e2b(ep):
132        return re.search(r'\s*(.*?)\s*=', ep).group(1).strip()
133
134    def e2s(ep, base='src'):
135        return (base+os.path.sep+re.search(r'.*=\s*(.*?):', ep).group(1).replace('.', '/')+'.py').strip()
136
137    def e2m(ep):
138        return re.search(r'.*=\s*(.*?)\s*:', ep).group(1).strip()
139
140    def e2f(ep):
141        return ep[ep.rindex(':')+1:].strip()
142
143    basenames, functions, modules, scripts = {}, {}, {}, {}
144    for x in ('console', 'gui'):
145        y = x + '_scripts'
146        basenames[x] = list(map(e2b, entry_points[y]))
147        functions[x] = list(map(e2f, entry_points[y]))
148        modules[x] = list(map(e2m, entry_points[y]))
149        scripts[x] = list(map(e2s, entry_points[y]))
150
151
152initialize_constants()
153preferred_encoding = 'utf-8'
154prints = print
155warnings = []
156
157
158def get_warnings():
159    return list(warnings)
160
161
162def edit_file(path):
163    return subprocess.Popen([
164        'vim', '-c', 'ALELint', '-c', 'ALEFirst', '-S', os.path.join(SRC, '../session.vim'), '-f', path
165    ]).wait() == 0
166
167
168class Command:
169
170    SRC = SRC
171    RESOURCES = os.path.join(os.path.dirname(SRC), 'resources')
172    description = ''
173
174    sub_commands = []
175
176    def __init__(self):
177        self.d = os.path.dirname
178        self.j = os.path.join
179        self.a = os.path.abspath
180        self.b = os.path.basename
181        self.s = os.path.splitext
182        self.e = os.path.exists
183        self.orig_euid = os.geteuid() if hasattr(os, 'geteuid') else None
184        self.real_uid = os.environ.get('SUDO_UID', None)
185        self.real_gid = os.environ.get('SUDO_GID', None)
186        self.real_user = os.environ.get('SUDO_USER', None)
187
188    def drop_privileges(self):
189        if not islinux or ismacos or isfreebsd:
190            return
191        if self.real_user is not None:
192            self.info('Dropping privileges to those of', self.real_user+':',
193                    self.real_uid)
194        if self.real_gid is not None:
195            os.setegid(int(self.real_gid))
196        if self.real_uid is not None:
197            os.seteuid(int(self.real_uid))
198
199    def regain_privileges(self):
200        if not islinux or ismacos or isfreebsd:
201            return
202        if os.geteuid() != 0 and self.orig_euid == 0:
203            self.info('Trying to get root privileges')
204            os.seteuid(0)
205            if os.getegid() != 0:
206                os.setegid(0)
207
208    def pre_sub_commands(self, opts):
209        pass
210
211    def running(self, cmd):
212        from setup.commands import command_names
213        if os.environ.get('CI'):
214            self.info('::group::' + command_names[cmd])
215        self.info('\n*')
216        self.info('* Running', command_names[cmd])
217        self.info('*\n')
218
219    def run_cmd(self, cmd, opts):
220        from setup.commands import command_names
221        cmd.pre_sub_commands(opts)
222        for scmd in cmd.sub_commands:
223            self.run_cmd(scmd, opts)
224
225        st = time.time()
226        self.running(cmd)
227        cmd.run(opts)
228        self.info('* %s took %.1f seconds' % (command_names[cmd], time.time() - st))
229        if os.environ.get('CI'):
230            self.info('::endgroup::')
231
232    def run_all(self, opts):
233        self.run_cmd(self, opts)
234
235    def add_command_options(self, command, parser):
236        import setup.commands as commands
237        command.sub_commands = [getattr(commands, cmd) for cmd in
238                command.sub_commands]
239        for cmd in command.sub_commands:
240            self.add_command_options(cmd, parser)
241
242        command.add_options(parser)
243
244    def add_all_options(self, parser):
245        self.add_command_options(self, parser)
246
247    def run(self, opts):
248        pass
249
250    def add_options(self, parser):
251        pass
252
253    def clean(self):
254        pass
255
256    @classmethod
257    def newer(cls, targets, sources):
258        '''
259        Return True if sources is newer that targets or if targets
260        does not exist.
261        '''
262        return newer(targets, sources)
263
264    def info(self, *args, **kwargs):
265        prints(*args, **kwargs)
266        sys.stdout.flush()
267
268    def warn(self, *args, **kwargs):
269        print('\n'+'_'*20, 'WARNING','_'*20)
270        prints(*args, **kwargs)
271        print('_'*50)
272        warnings.append((args, kwargs))
273        sys.stdout.flush()
274
275    @contextmanager
276    def temp_dir(self, **kw):
277        ans = tempfile.mkdtemp(**kw)
278        try:
279            yield ans
280        finally:
281            shutil.rmtree(ans)
282
283
284def installer_name(ext, is64bit=False):
285    if is64bit and ext == 'msi':
286        return 'dist/%s-64bit-%s.msi'%(__appname__, __version__)
287    if ext in ('exe', 'msi'):
288        return 'dist/%s-%s.%s'%(__appname__, __version__, ext)
289    if ext == 'dmg':
290        if is64bit:
291            return 'dist/%s-%s-x86_64.%s'%(__appname__, __version__, ext)
292        return 'dist/%s-%s.%s'%(__appname__, __version__, ext)
293
294    ans = 'dist/%s-%s-i686.%s'%(__appname__, __version__, ext)
295    if is64bit:
296        ans = ans.replace('i686', 'x86_64')
297    return ans
298