Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/tools/utils.py
6171 views
1
# Copyright 2020 The Emscripten Authors. All rights reserved.
2
# Emscripten is available under two separate licenses, the MIT license and the
3
# University of Illinois/NCSA Open Source License. Both these licenses can be
4
# found in the LICENSE file.
5
6
"""General purpose utility functions. The code in this file should mostly be
7
not emscripten-specific, but general purpose enough to be useful in any command
8
line utility."""
9
10
import functools
11
import logging
12
import os
13
import shlex
14
import shutil
15
import stat
16
import subprocess
17
import sys
18
from pathlib import Path
19
20
from . import diagnostics
21
22
__rootpath__ = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
23
WINDOWS = sys.platform.startswith('win')
24
MACOS = sys.platform == 'darwin'
25
LINUX = sys.platform.startswith('linux')
26
27
logger = logging.getLogger('utils')
28
29
30
def run_process(cmd, check=True, input=None, *args, **kw):
31
"""Runs a subprocess returning the exit code.
32
33
By default this function will raise an exception on failure. Therefore this should only be
34
used if you want to handle such failures. For most subprocesses, failures are not recoverable
35
and should be fatal. In those cases the `check_call` wrapper should be preferred.
36
"""
37
38
# Flush standard streams otherwise the output of the subprocess may appear in the
39
# output before messages that we have already written.
40
sys.stdout.flush()
41
sys.stderr.flush()
42
kw.setdefault('text', True)
43
kw.setdefault('encoding', 'utf-8')
44
ret = subprocess.run(cmd, check=check, input=input, *args, **kw)
45
debug_text = '%sexecuted %s' % ('successfully ' if check else '', shlex.join(cmd))
46
logger.debug(debug_text)
47
return ret
48
49
50
def exec(cmd):
51
if WINDOWS:
52
rtn = run_process(cmd, stdin=sys.stdin, check=False).returncode
53
sys.exit(rtn)
54
else:
55
sys.stdout.flush()
56
sys.stderr.flush()
57
os.execvp(cmd[0], cmd)
58
59
60
def exit_with_error(msg, *args):
61
diagnostics.error(msg, *args)
62
63
64
def path_from_root(*pathelems):
65
return str(Path(__rootpath__, *pathelems))
66
67
68
def exe_path_from_root(*pathelems):
69
return find_exe(path_from_root(*pathelems))
70
71
72
def suffix(name):
73
"""Return the file extension"""
74
return os.path.splitext(name)[1]
75
76
77
def find_exe(*pathelems):
78
path = os.path.join(*pathelems)
79
80
if WINDOWS:
81
# Should we use PATHEXT environment variable here?
82
# For now, specify only enough extensions to find llvm / binaryen / emscripten executables.
83
for ext in ['.exe', '.bat']:
84
if os.path.isfile(path + ext):
85
return path + ext
86
87
return path
88
89
90
def replace_suffix(filename, new_suffix):
91
assert new_suffix[0] == '.'
92
return os.path.splitext(filename)[0] + new_suffix
93
94
95
def unsuffixed(name):
96
"""Return the filename without the extension.
97
98
If there are multiple extensions this strips only the final one.
99
"""
100
return os.path.splitext(name)[0]
101
102
103
def unsuffixed_basename(name):
104
return os.path.basename(unsuffixed(name))
105
106
107
def get_file_suffix(filename):
108
"""Parses the essential suffix of a filename, discarding Unix-style version
109
numbers in the name. For example for 'libz.so.1.2.8' returns '.so'"""
110
while filename:
111
filename, suffix = os.path.splitext(filename)
112
if not suffix[1:].isdigit():
113
return suffix
114
return ''
115
116
117
def normalize_path(path):
118
"""Normalize path separators to UNIX-style forward slashes.
119
120
This can be useful when converting paths to URLs or JS strings,
121
or when trying to generate consistent output file contents
122
across all platforms. In most cases UNIX-style separators work
123
fine on windows.
124
"""
125
return path.replace('\\', '/').replace('//', '/')
126
127
128
def safe_ensure_dirs(dirname):
129
os.makedirs(dirname, exist_ok=True)
130
131
132
def make_writable(filename):
133
assert os.path.exists(filename)
134
old_mode = stat.S_IMODE(os.stat(filename).st_mode)
135
os.chmod(filename, old_mode | stat.S_IWUSR)
136
137
138
def safe_copy(src, dst):
139
logger.debug('copy: %s -> %s', src, dst)
140
src = os.path.abspath(src)
141
dst = os.path.abspath(dst)
142
if os.path.isdir(dst):
143
dst = os.path.join(dst, os.path.basename(src))
144
if src == dst:
145
return
146
if dst == os.devnull:
147
return
148
# Copies data and permission bits, but not other metadata such as timestamp
149
shutil.copy(src, dst)
150
# We always want the target file to be writable even when copying from
151
# read-only source. (e.g. a read-only install of emscripten).
152
make_writable(dst)
153
154
155
def convert_line_endings_in_file(filename, to_eol):
156
if to_eol == os.linesep:
157
assert os.path.exists(filename)
158
return # No conversion needed
159
160
text = read_file(filename)
161
write_file(filename, text, line_endings=to_eol)
162
163
164
def read_file(file_path):
165
"""Read from a file opened in text mode"""
166
with open(file_path, encoding='utf-8') as fh:
167
return fh.read()
168
169
170
def read_binary(file_path):
171
"""Read from a file opened in binary mode"""
172
with open(file_path, 'rb') as fh:
173
return fh.read()
174
175
176
def write_file(file_path, text, line_endings=None):
177
"""Write to a file opened in text mode"""
178
if line_endings and line_endings != os.linesep:
179
text = text.replace('\n', line_endings)
180
write_binary(file_path, text.encode('utf-8'))
181
else:
182
with open(file_path, 'w', encoding='utf-8') as fh:
183
fh.write(text)
184
185
186
def write_binary(file_path, contents):
187
"""Write to a file opened in binary mode"""
188
with open(file_path, 'wb') as fh:
189
fh.write(contents)
190
191
192
def delete_file(filename):
193
"""Delete a file (if it exists)."""
194
if os.path.lexists(filename):
195
os.remove(filename)
196
197
198
def delete_dir(dirname):
199
"""Delete a directory (if it exists)."""
200
if not os.path.exists(dirname):
201
return
202
shutil.rmtree(dirname)
203
204
205
def delete_contents(dirname, exclude=None):
206
"""Delete the contents of a directory without removing
207
the directory itself."""
208
if not os.path.exists(dirname):
209
return
210
for entry in os.listdir(dirname):
211
if exclude and entry in exclude:
212
continue
213
entry = os.path.join(dirname, entry)
214
if os.path.isdir(entry):
215
delete_dir(entry)
216
else:
217
delete_file(entry)
218
219
220
def get_num_cores():
221
# Prefer `os.process_cpu_count` when available (3.13 and above) since
222
# it takes into account thread affinity.
223
# Fall back to `os.sched_getaffinity` where available and finally
224
# `os.cpu_count`, which should work everywhere.
225
if hasattr(os, 'process_cpu_count'):
226
cpu_count = os.process_cpu_count()
227
elif hasattr(os, 'sched_getaffinity'):
228
cpu_count = len(os.sched_getaffinity(0))
229
else:
230
cpu_count = os.cpu_count()
231
return int(os.environ.get('EMCC_CORES', cpu_count))
232
233
234
memoize = functools.cache
235
236
237
# TODO: Move this back to shared.py once importing that file becoming side effect free (i.e. it no longer requires a config).
238
def set_version_globals():
239
global EMSCRIPTEN_VERSION, EMSCRIPTEN_VERSION_MAJOR, EMSCRIPTEN_VERSION_MINOR, EMSCRIPTEN_VERSION_TINY
240
filename = path_from_root('emscripten-version.txt')
241
EMSCRIPTEN_VERSION = read_file(filename).strip().strip('"')
242
parts = [int(x) for x in EMSCRIPTEN_VERSION.split('-')[0].split('.')]
243
EMSCRIPTEN_VERSION_MAJOR, EMSCRIPTEN_VERSION_MINOR, EMSCRIPTEN_VERSION_TINY = parts
244
245