1# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
2# vim: set filetype=python:
3# This Source Code Form is subject to the terms of the Mozilla Public
4# License, v. 2.0. If a copy of the MPL was not distributed with this
5# file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7
8@imports("sys")
9def die(*args):
10    "Print an error and terminate configure."
11    log.error(*args)
12    sys.exit(1)
13
14
15@imports(_from="mozbuild.configure", _import="ConfigureError")
16def configure_error(message):
17    """Raise a programming error and terminate configure.
18    Primarily for use in moz.configure templates to sanity check
19    their inputs from moz.configure usage."""
20    raise ConfigureError(message)
21
22
23# A wrapper to obtain a process' output and return code.
24# Returns a tuple (retcode, stdout, stderr).
25@imports("os")
26@imports("six")
27@imports("subprocess")
28@imports(_from="mozbuild.shellutil", _import="quote")
29@imports(_from="mozbuild.util", _import="system_encoding")
30def get_cmd_output(*args, **kwargs):
31    log.debug("Executing: `%s`", quote(*args))
32    proc = subprocess.Popen(
33        args,
34        stdout=subprocess.PIPE,
35        stderr=subprocess.PIPE,
36        # On Python 2 on Windows, close_fds prevents the process from inheriting
37        # stdout/stderr. Elsewhere, it simply prevents it from inheriting extra
38        # file descriptors, which is what we want.
39        close_fds=os.name != "nt",
40        **kwargs
41    )
42    stdout, stderr = proc.communicate()
43    # Normally we would set the `encoding` and `errors` arguments in the
44    # constructor to subprocess.Popen, but those arguments were added in 3.6
45    # and we need to support back to 3.5, so instead we need to do this
46    # nonsense.
47    stdout = six.ensure_text(
48        stdout, encoding=system_encoding, errors="replace"
49    ).replace("\r\n", "\n")
50    stderr = six.ensure_text(
51        stderr, encoding=system_encoding, errors="replace"
52    ).replace("\r\n", "\n")
53    return proc.wait(), stdout, stderr
54
55
56# A wrapper to obtain a process' output that returns the output generated
57# by running the given command if it exits normally, and streams that
58# output to log.debug and calls die or the given error callback if it
59# does not.
60@imports(_from="mozbuild.configure.util", _import="LineIO")
61@imports(_from="mozbuild.shellutil", _import="quote")
62def check_cmd_output(*args, **kwargs):
63    onerror = kwargs.pop("onerror", None)
64
65    with log.queue_debug():
66        retcode, stdout, stderr = get_cmd_output(*args, **kwargs)
67        if retcode == 0:
68            return stdout
69
70        log.debug("The command returned non-zero exit status %d.", retcode)
71        for out, desc in ((stdout, "output"), (stderr, "error output")):
72            if out:
73                log.debug("Its %s was:", desc)
74                with LineIO(lambda l: log.debug("| %s", l)) as o:
75                    o.write(out)
76        if onerror:
77            return onerror()
78        die("Command `%s` failed with exit status %d." % (quote(*args), retcode))
79
80
81@imports("os")
82def is_absolute_or_relative(path):
83    if os.altsep and os.altsep in path:
84        return True
85    return os.sep in path
86
87
88@imports(_import="mozpack.path", _as="mozpath")
89def normsep(path):
90    return mozpath.normsep(path)
91
92
93@imports("ctypes")
94@imports(_from="ctypes", _import="wintypes")
95@imports(_from="mozbuild.configure.constants", _import="WindowsBinaryType")
96def windows_binary_type(path):
97    """Obtain the type of a binary on Windows.
98
99    Returns WindowsBinaryType constant.
100    """
101    GetBinaryTypeW = ctypes.windll.kernel32.GetBinaryTypeW
102    GetBinaryTypeW.argtypes = [wintypes.LPWSTR, ctypes.POINTER(wintypes.DWORD)]
103    GetBinaryTypeW.restype = wintypes.BOOL
104
105    bin_type = wintypes.DWORD()
106    res = GetBinaryTypeW(path, ctypes.byref(bin_type))
107    if not res:
108        die("could not obtain binary type of %s" % path)
109
110    if bin_type.value == 0:
111        return WindowsBinaryType("win32")
112    elif bin_type.value == 6:
113        return WindowsBinaryType("win64")
114    # If we see another binary type, something is likely horribly wrong.
115    else:
116        die("unsupported binary type on %s: %s" % (path, bin_type))
117
118
119@imports("ctypes")
120@imports(_from="ctypes", _import="wintypes")
121def get_GetShortPathNameW():
122    GetShortPathNameW = ctypes.windll.kernel32.GetShortPathNameW
123    GetShortPathNameW.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD]
124    GetShortPathNameW.restype = wintypes.DWORD
125    return GetShortPathNameW
126
127
128@template
129@imports("ctypes")
130@imports("platform")
131@imports(_from="mozbuild.shellutil", _import="quote")
132def normalize_path():
133    # Until the build system can properly handle programs that need quoting,
134    # transform those paths into their short version on Windows (e.g.
135    # c:\PROGRA~1...).
136    if platform.system() == "Windows":
137        GetShortPathNameW = get_GetShortPathNameW()
138
139        def normalize_path(path):
140            path = normsep(path)
141            if quote(path) == path:
142                return path
143            size = 0
144            while True:
145                out = ctypes.create_unicode_buffer(size)
146                needed = GetShortPathNameW(path, out, size)
147                if size >= needed:
148                    if " " in out.value:
149                        die(
150                            "GetShortPathName returned a long path name: `%s`. "
151                            "Use `fsutil file setshortname' "
152                            "to create a short name "
153                            "for any components of this path "
154                            "that have spaces.",
155                            out.value,
156                        )
157                    return normsep(out.value)
158                size = needed
159
160    else:
161
162        def normalize_path(path):
163            return normsep(path)
164
165    return normalize_path
166
167
168normalize_path = normalize_path()
169
170
171# Locates the given program using which, or returns the given path if it
172# exists.
173# The `paths` parameter may be passed to search the given paths instead of
174# $PATH.
175@imports("sys")
176@imports(_from="os", _import="pathsep")
177@imports(_from="os", _import="environ")
178@imports(_from="mozfile", _import="which")
179def find_program(file, paths=None):
180    # The following snippet comes from `which` itself, with a slight
181    # modification to use lowercase extensions, because it's confusing rustup
182    # (on top of making results not really appealing to the eye).
183
184    # Windows has the concept of a list of extensions (PATHEXT env var).
185    if sys.platform.startswith("win"):
186        exts = [e.lower() for e in environ.get("PATHEXT", "").split(pathsep)]
187        # If '.exe' is not in exts then obviously this is Win9x and
188        # or a bogus PATHEXT, then use a reasonable default.
189        if ".exe" not in exts:
190            exts = [".com", ".exe", ".bat"]
191    else:
192        exts = None
193
194    if is_absolute_or_relative(file):
195        path = which(os.path.basename(file), path=os.path.dirname(file), exts=exts)
196        return normalize_path(path) if path else None
197
198    if paths:
199        if not isinstance(paths, (list, tuple)):
200            die(
201                "Paths provided to find_program must be a list of strings, " "not %r",
202                paths,
203            )
204        paths = pathsep.join(paths)
205
206    path = which(file, path=paths, exts=exts)
207    return normalize_path(path) if path else None
208
209
210@imports("os")
211@imports(_from="mozbuild.configure.util", _import="LineIO")
212@imports(_from="six", _import="ensure_binary")
213@imports(_from="tempfile", _import="mkstemp")
214def try_invoke_compiler(compiler, language, source, flags=None, onerror=None):
215    flags = flags or []
216
217    if not isinstance(flags, (list, tuple)):
218        die("Flags provided to try_compile must be a list of strings, " "not %r", flags)
219
220    suffix = {
221        "C": ".c",
222        "C++": ".cpp",
223    }[language]
224
225    fd, path = mkstemp(prefix="conftest.", suffix=suffix, text=True)
226    try:
227        source = source.encode("ascii", "replace")
228
229        log.debug("Creating `%s` with content:", path)
230        with LineIO(lambda l: log.debug("| %s", l)) as out:
231            out.write(source)
232
233        os.write(fd, ensure_binary(source))
234        os.close(fd)
235        cmd = compiler + [path] + list(flags)
236        kwargs = {"onerror": onerror}
237        return check_cmd_output(*cmd, **kwargs)
238    finally:
239        os.remove(path)
240
241
242def unique_list(l):
243    result = []
244    for i in l:
245        if l not in result:
246            result.append(i)
247    return result
248
249
250# Get values out of the Windows registry. This function can only be called on
251# Windows.
252# The `pattern` argument is a string starting with HKEY_ and giving the full
253# "path" of the registry key to get the value for, with backslash separators.
254# The string can contains wildcards ('*').
255# The result of this functions is an enumerator yielding tuples for each
256# match. Each of these tuples contains the key name matching wildcards
257# followed by the value.
258#
259# The `get_32_and_64_bit` argument is a boolean, if True then it will return the
260# values from the 32-bit and 64-bit registry views. This defaults to False,
261# which will return the view depending on the bitness of python.
262#
263# Examples:
264#   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
265#                       r'Windows Kits\Installed Roots\KitsRoot*')
266#   yields e.g.:
267#     ('KitsRoot81', r'C:\Program Files (x86)\Windows Kits\8.1\')
268#     ('KitsRoot10', r'C:\Program Files (x86)\Windows Kits\10\')
269#
270#   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
271#                       r'Windows Kits\Installed Roots\KitsRoot8.1')
272#   yields e.g.:
273#     (r'C:\Program Files (x86)\Windows Kits\8.1\',)
274#
275#   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
276#                       r'Windows Kits\Installed Roots\KitsRoot8.1',
277#                       get_32_and_64_bit=True)
278#   yields e.g.:
279#     (r'C:\Program Files (x86)\Windows Kits\8.1\',)
280#     (r'C:\Program Files\Windows Kits\8.1\',)
281#
282#   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
283#                       r'Windows Kits\*\KitsRoot*')
284#   yields e.g.:
285#     ('Installed Roots', 'KitsRoot81',
286#      r'C:\Program Files (x86)\Windows Kits\8.1\')
287#     ('Installed Roots', 'KitsRoot10',
288#      r'C:\Program Files (x86)\Windows Kits\10\')
289#
290#   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
291#                       r'VisualStudio\VC\*\x86\*\Compiler')
292#   yields e.g.:
293#     ('19.0', 'arm', r'C:\...\amd64_arm\cl.exe')
294#     ('19.0', 'x64', r'C:\...\amd64\cl.exe')
295#     ('19.0', 'x86', r'C:\...\amd64_x86\cl.exe')
296@imports(_import="winreg")
297@imports(_from="__builtin__", _import="WindowsError")
298@imports(_from="fnmatch", _import="fnmatch")
299def get_registry_values(pattern, get_32_and_64_bit=False):
300    def enum_helper(func, key):
301        i = 0
302        while True:
303            try:
304                yield func(key, i)
305            except WindowsError:
306                break
307            i += 1
308
309    def get_keys(key, pattern, access_mask):
310        try:
311            s = winreg.OpenKey(key, "\\".join(pattern[:-1]), 0, access_mask)
312        except WindowsError:
313            return
314        for k in enum_helper(winreg.EnumKey, s):
315            if fnmatch(k, pattern[-1]):
316                try:
317                    yield k, winreg.OpenKey(s, k, 0, access_mask)
318                except WindowsError:
319                    pass
320
321    def get_values(key, pattern, access_mask):
322        try:
323            s = winreg.OpenKey(key, "\\".join(pattern[:-1]), 0, access_mask)
324        except WindowsError:
325            return
326        for k, v, t in enum_helper(winreg.EnumValue, s):
327            if fnmatch(k, pattern[-1]):
328                yield k, v
329
330    def split_pattern(pattern):
331        subpattern = []
332        for p in pattern:
333            subpattern.append(p)
334            if "*" in p:
335                yield subpattern
336                subpattern = []
337        if subpattern:
338            yield subpattern
339
340    def get_all_values(keys, pattern, access_mask):
341        for i, p in enumerate(pattern):
342            next_keys = []
343            for base_key in keys:
344                matches = base_key[:-1]
345                base_key = base_key[-1]
346                if i == len(pattern) - 1:
347                    want_name = "*" in p[-1]
348                    for name, value in get_values(base_key, p, access_mask):
349                        yield matches + ((name, value) if want_name else (value,))
350                else:
351                    for name, k in get_keys(base_key, p, access_mask):
352                        next_keys.append(matches + (name, k))
353            keys = next_keys
354
355    pattern = pattern.split("\\")
356    assert pattern[0].startswith("HKEY_")
357    keys = [(getattr(winreg, pattern[0]),)]
358    pattern = list(split_pattern(pattern[1:]))
359    if get_32_and_64_bit:
360        for match in get_all_values(
361            keys, pattern, winreg.KEY_READ | winreg.KEY_WOW64_32KEY
362        ):
363            yield match
364        for match in get_all_values(
365            keys, pattern, winreg.KEY_READ | winreg.KEY_WOW64_64KEY
366        ):
367            yield match
368    else:
369        for match in get_all_values(keys, pattern, winreg.KEY_READ):
370            yield match
371
372
373@imports(_from="mozbuild.configure.util", _import="Version", _as="_Version")
374def Version(v):
375    "A version number that can be compared usefully."
376    return _Version(v)
377
378
379# Denotes a deprecated option. Combines option() and @depends:
380# @deprecated_option('--option')
381# def option(value):
382#     ...
383# @deprecated_option() takes the same arguments as option(), except `help`.
384# The function may handle the option like a typical @depends function would,
385# but it is recommended it emits a deprecation error message suggesting an
386# alternative option to use if there is one.
387
388
389@template
390def deprecated_option(*args, **kwargs):
391    assert "help" not in kwargs
392    kwargs["help"] = "Deprecated"
393    opt = option(*args, **kwargs)
394
395    def decorator(func):
396        @depends(opt.option)
397        def deprecated(value):
398            if value.origin != "default":
399                return func(value)
400
401        return deprecated
402
403    return decorator
404
405
406# Turn an object into an object that can be used as an argument to @depends.
407# The given object can be a literal value, a function that takes no argument,
408# or, for convenience, a @depends function.
409@template
410@imports(_from="inspect", _import="isfunction")
411@imports(_from="mozbuild.configure", _import="SandboxDependsFunction")
412def dependable(obj):
413    if isinstance(obj, SandboxDependsFunction):
414        return obj
415    if isfunction(obj):
416        return depends(when=True)(obj)
417    # Depend on --help to make lint happy if the dependable is used as an input
418    # to an option().
419    return depends("--help", when=True)(lambda _: obj)
420
421
422always = dependable(True)
423never = dependable(False)
424
425
426# Create a decorator that will only execute the body of a function
427# if the passed function returns True when passed all positional
428# arguments.
429@template
430def depends_tmpl(eval_args_fn, *args, **kwargs):
431    if kwargs:
432        assert len(kwargs) == 1
433        when = kwargs["when"]
434    else:
435        when = None
436
437    def decorator(func):
438        @depends(*args, when=when)
439        def wrapper(*args):
440            if eval_args_fn(args):
441                return func(*args)
442
443        return wrapper
444
445    return decorator
446
447
448# Like @depends, but the decorated function is only called if one of the
449# arguments it would be called with has a positive value (bool(value) is True)
450@template
451def depends_if(*args, **kwargs):
452    return depends_tmpl(any, *args, **kwargs)
453
454
455# Like @depends, but the decorated function is only called if all of the
456# arguments it would be called with have a positive value.
457@template
458def depends_all(*args, **kwargs):
459    return depends_tmpl(all, *args, **kwargs)
460
461
462# Hacks related to old-configure
463# ==============================
464
465
466@dependable
467def old_configure_assignments():
468    return []
469
470
471@template
472def add_old_configure_assignment(var, value, when=None):
473    var = dependable(var)
474    value = dependable(value)
475
476    @depends(old_configure_assignments, var, value, when=when)
477    @imports(_from="mozbuild.shellutil", _import="quote")
478    def add_assignment(assignments, var, value):
479        if var is None or value is None:
480            return
481        if value is True:
482            assignments.append((var, "1"))
483        elif value is False:
484            assignments.append((var, ""))
485        else:
486            if isinstance(value, (list, tuple)):
487                value = quote(*value)
488            assignments.append((var, str(value)))
489