1import os
2import sys
3import time
4
5from subprocess import CalledProcessError, check_call
6from typing import List, IO, Optional, Tuple
7
8
9def which(command: str, paths: Optional[str] = None) -> Optional[str]:
10    """which(command, [paths]) - Look up the given command in the paths string
11    (or the PATH environment variable, if unspecified)."""
12
13    if paths is None:
14        paths = os.environ.get('PATH', '')
15
16    # Check for absolute match first.
17    if os.path.exists(command):
18        return command
19
20    # Would be nice if Python had a lib function for this.
21    if not paths:
22        paths = os.defpath
23
24    # Get suffixes to search.
25    # On Cygwin, 'PATHEXT' may exist but it should not be used.
26    if os.pathsep == ';':
27        pathext = os.environ.get('PATHEXT', '').split(';')
28    else:
29        pathext = ['']
30
31    # Search the paths...
32    for path in paths.split(os.pathsep):
33        for ext in pathext:
34            p = os.path.join(path, command + ext)
35            if os.path.exists(p):
36                return p
37
38    return None
39
40
41def has_no_extension(file_name: str) -> bool:
42    root, ext = os.path.splitext(file_name)
43    return ext == ""
44
45
46def is_valid_single_input_file(file_name: str) -> bool:
47    root, ext = os.path.splitext(file_name)
48    return ext in (".i", ".ii", ".c", ".cpp", ".m", "")
49
50
51def time_to_str(time: float) -> str:
52    """
53    Convert given time in seconds into a human-readable string.
54    """
55    return f"{time:.2f}s"
56
57
58def memory_to_str(memory: int) -> str:
59    """
60    Convert given number of bytes into a human-readable string.
61    """
62    if memory:
63        try:
64            import humanize
65            return humanize.naturalsize(memory, gnu=True)
66        except ImportError:
67            # no formatter installed, let's keep it in bytes
68            return f"{memory}B"
69
70    # If memory is 0, we didn't succeed measuring it.
71    return "N/A"
72
73
74def check_and_measure_call(*popenargs, **kwargs) -> Tuple[float, int]:
75    """
76    Run command with arguments.  Wait for command to complete and measure
77    execution time and peak memory consumption.
78    If the exit code was zero then return, otherwise raise
79    CalledProcessError.  The CalledProcessError object will have the
80    return code in the returncode attribute.
81
82    The arguments are the same as for the call and check_call functions.
83
84    Return a tuple of execution time and peak memory.
85    """
86    peak_mem = 0
87    start_time = time.time()
88
89    try:
90        import psutil as ps
91
92        def get_memory(process: ps.Process) -> int:
93            mem = 0
94
95            # we want to gather memory usage from all of the child processes
96            descendants = list(process.children(recursive=True))
97            descendants.append(process)
98
99            for subprocess in descendants:
100                try:
101                    mem += subprocess.memory_info().rss
102                except (ps.NoSuchProcess, ps.AccessDenied):
103                    continue
104
105            return mem
106
107        with ps.Popen(*popenargs, **kwargs) as process:
108            # while the process is running calculate resource utilization.
109            while (process.is_running() and
110                   process.status() != ps.STATUS_ZOMBIE):
111                # track the peak utilization of the process
112                peak_mem = max(peak_mem, get_memory(process))
113                time.sleep(.5)
114
115            if process.is_running():
116                process.kill()
117
118        if process.returncode != 0:
119            cmd = kwargs.get("args")
120            if cmd is None:
121                cmd = popenargs[0]
122            raise CalledProcessError(process.returncode, cmd)
123
124    except ImportError:
125        # back off to subprocess if we don't have psutil installed
126        peak_mem = 0
127        check_call(*popenargs, **kwargs)
128
129    return time.time() - start_time, peak_mem
130
131
132def run_script(script_path: str, build_log_file: IO, cwd: str,
133               out=sys.stdout, err=sys.stderr, verbose: int = 0):
134    """
135    Run the provided script if it exists.
136    """
137    if os.path.exists(script_path):
138        try:
139            if verbose == 1:
140                out.write(f"  Executing: {script_path}\n")
141
142            check_call(f"chmod +x '{script_path}'", cwd=cwd,
143                       stderr=build_log_file,
144                       stdout=build_log_file,
145                       shell=True)
146
147            check_call(f"'{script_path}'", cwd=cwd,
148                       stderr=build_log_file,
149                       stdout=build_log_file,
150                       shell=True)
151
152        except CalledProcessError:
153            err.write(f"Error: Running {script_path} failed. "
154                      f"See {build_log_file.name} for details.\n")
155            sys.exit(-1)
156
157
158def is_comment_csv_line(entries: List[str]) -> bool:
159    """
160    Treat CSV lines starting with a '#' as a comment.
161    """
162    return len(entries) > 0 and entries[0].startswith("#")
163