aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/project/utils.py
blob: 39dfb0dbae644ced77c1f17d245b47668d055187 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com>
# This file is part of the "cmake-common" project.
# For details, see https://github.com/egor-tensin/cmake-common.
# Distributed under the MIT License.

from contextlib import contextmanager
import functools
import logging
import os.path
import shutil
import subprocess
import sys
import tempfile
import time

from project.os import on_cygwin, on_linux


def normalize_path(s):
    return os.path.abspath(os.path.normpath(s))


def mkdir_parent(path):
    os.makedirs(path, exist_ok=True)


def full_exe_name(exe):
    if on_linux():
        # There's no PATHEXT on Linux.
        return exe
    # b2 on Windows/Cygwin doesn't like it when the executable name doesn't
    # include the extension.
    dir_path = os.path.dirname(exe) or None
    path = shutil.which(exe, path=dir_path)
    if not path:
        raise RuntimeError(f"executable '{exe}' could not be found")
    if on_cygwin():
        # On Cygwin, shutil.which('gcc') == '/usr/bin/gcc' and shutil.which('gcc.exe')
        # == '/usr/bin/gcc.exe'; we want the latter version.  shutil.which('clang++')
        # == '/usr/bin/clang++' is fine though, since it _is_ the complete path
        # (clang++ is a symlink).
        if os.path.exists(path) and os.path.exists(path + '.exe'):
            path += '.exe'
    if dir_path:
        # If it was found in a specific directory, include the directory in the
        # result.  shutil.which returns the executable name prefixed with the
        # path argument.
        return path
    # If it was found in PATH, just return the basename (which includes the
    # extension).
    return os.path.basename(path)


@contextmanager
def setup_logging():
    logging.basicConfig(
        format='%(asctime)s | %(levelname)s | %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S',
        level=logging.INFO,
        # Log to stdout, because that's where subprocess's output goes (so that
        # they don't get interleaved).
        stream=sys.stdout)
    try:
        yield
    except Exception as e:
        logging.exception(e)
        sys.exit(1)


@contextmanager
def cd(path):
    cwd = os.getcwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(cwd)


def run(cmd_line, **kwargs):
    logging.info('Running executable: %s', cmd_line)
    return subprocess.run(cmd_line, check=True, **kwargs)


@contextmanager
def delete_on_error(path):
    try:
        yield
    except:
        logging.info('Removing temporary file: %s', path)
        os.remove(path)
        raise


@contextmanager
def delete(path):
    try:
        yield
    finally:
        logging.info('Removing temporary file: %s', path)
        os.remove(path)


@contextmanager
def temp_file(**kwargs):
    '''Make NamedTemporaryFile usable on Windows.

    It can't be opened a second time on Windows, hence this silliness.
    '''
    tmp = tempfile.NamedTemporaryFile(delete=False, **kwargs)
    with tmp as file, delete_on_error(file.name):
        path = file.name
        logging.info('Created temporary file: %s', path)
    with delete(path):
        yield path


def env(name):
    if name not in os.environ:
        raise RuntimeError(f'undefined environment variable: {name}')
    return os.environ[name]


def retry(exc_type, timeout=5, tries=3, backoff=2):
    def wrapper(func):
        @functools.wraps(func)
        def func2(*args, **kwargs):
            current_timeout = timeout
            current_try = 0
            while True:
                try:
                    return func(*args, **kwargs)
                except exc_type as e:
                    logging.exception(e)
                    current_try += 1
                    if current_try < tries:
                        logging.error('Retrying after %d seconds', current_timeout)
                        time.sleep(current_timeout)
                        current_timeout *= backoff
                        continue
                    raise
        return func2
    return wrapper