diff options
Diffstat (limited to 'build/moz.configure/util.configure')
-rw-r--r-- | build/moz.configure/util.configure | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/build/moz.configure/util.configure b/build/moz.configure/util.configure new file mode 100644 index 0000000000..d93b91fcdd --- /dev/null +++ b/build/moz.configure/util.configure @@ -0,0 +1,550 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +@imports("sys") +def die(*args): + "Print an error and terminate configure." + log.error(*args) + sys.exit(1) + + +@imports(_from="mozbuild.configure", _import="ConfigureError") +def configure_error(message): + """Raise a programming error and terminate configure. + Primarily for use in moz.configure templates to sanity check + their inputs from moz.configure usage.""" + raise ConfigureError(message) + + +# A wrapper to obtain a process' output and return code. +# Returns a tuple (retcode, stdout, stderr). +@imports("os") +@imports("subprocess") +@imports(_from="mozbuild.shellutil", _import="quote") +@imports(_from="mozbuild.util", _import="system_encoding") +def get_cmd_output(*args, **kwargs): + log.debug("Executing: `%s`", quote(*args)) + proc = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + # On Python 2 on Windows, close_fds prevents the process from inheriting + # stdout/stderr. Elsewhere, it simply prevents it from inheriting extra + # file descriptors, which is what we want. + close_fds=os.name != "nt", + encoding=system_encoding, + errors="replace", + **kwargs, + ) + stdout, stderr = proc.communicate() + return proc.wait(), stdout, stderr + + +# A wrapper to obtain a process' output that returns the output generated +# by running the given command if it exits normally, and streams that +# output to log.debug and calls die or the given error callback if it +# does not. +@imports(_from="mozbuild.configure.util", _import="LineIO") +@imports(_from="mozbuild.shellutil", _import="quote") +def check_cmd_output(*args, **kwargs): + onerror = kwargs.pop("onerror", None) + + with log.queue_debug(): + retcode, stdout, stderr = get_cmd_output(*args, **kwargs) + if retcode == 0: + with LineIO(lambda l: log.debug("| %s", l)) as o: + o.write(stderr) + return stdout + + log.debug("The command returned non-zero exit status %d.", retcode) + for out, desc in ((stdout, "output"), (stderr, "error output")): + if out: + log.debug("Its %s was:", desc) + with LineIO(lambda l: log.debug("| %s", l)) as o: + o.write(out) + if onerror: + return onerror() + die("Command `%s` failed with exit status %d." % (quote(*args), retcode)) + + +@imports("os") +def is_absolute_or_relative(path): + if os.altsep and os.altsep in path: + return True + return os.sep in path + + +@imports(_import="mozpack.path", _as="mozpath") +def normsep(path): + return mozpath.normsep(path) + + +@imports("ctypes") +@imports(_from="ctypes", _import="wintypes") +@imports(_from="mozbuild.configure.constants", _import="WindowsBinaryType") +def windows_binary_type(path): + """Obtain the type of a binary on Windows. + + Returns WindowsBinaryType constant. + """ + GetBinaryTypeW = ctypes.windll.kernel32.GetBinaryTypeW + GetBinaryTypeW.argtypes = [wintypes.LPWSTR, ctypes.POINTER(wintypes.DWORD)] + GetBinaryTypeW.restype = wintypes.BOOL + + bin_type = wintypes.DWORD() + res = GetBinaryTypeW(path, ctypes.byref(bin_type)) + if not res: + die("could not obtain binary type of %s" % path) + + if bin_type.value == 0: + return WindowsBinaryType("win32") + elif bin_type.value == 6: + return WindowsBinaryType("win64") + # If we see another binary type, something is likely horribly wrong. + else: + die("unsupported binary type on %s: %s" % (path, bin_type)) + + +@imports("ctypes") +@imports(_from="ctypes", _import="wintypes") +def get_GetShortPathNameW(): + GetShortPathNameW = ctypes.windll.kernel32.GetShortPathNameW + GetShortPathNameW.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD] + GetShortPathNameW.restype = wintypes.DWORD + return GetShortPathNameW + + +@template +@imports("ctypes") +@imports("platform") +@imports(_from="mozbuild.shellutil", _import="quote") +def normalize_path(): + # Until the build system can properly handle programs that need quoting, + # transform those paths into their short version on Windows (e.g. + # c:\PROGRA~1...). + if platform.system() == "Windows": + GetShortPathNameW = get_GetShortPathNameW() + + def normalize_path(path): + path = normsep(path) + if quote(path) == path: + return path + size = 0 + while True: + out = ctypes.create_unicode_buffer(size) + needed = GetShortPathNameW(path, out, size) + if size >= needed: + if " " in out.value: + die( + "GetShortPathName returned a long path name: `%s`. " + "Use `fsutil file setshortname' " + "to create a short name " + "for any components of this path " + "that have spaces.", + out.value, + ) + return normsep(out.value) + size = needed + + else: + + def normalize_path(path): + return normsep(path) + + return normalize_path + + +normalize_path = normalize_path() + + +# Locates the given program using which, or returns the given path if it +# exists. +# The `paths` parameter may be passed to search the given paths instead of +# $PATH. +@imports("sys") +@imports(_from="os", _import="pathsep") +@imports(_from="os", _import="environ") +@imports(_from="mozfile", _import="which") +def find_program(file, paths=None, allow_spaces=False): + def which_normalize(file, path, exts): + path = which(file, path=path, exts=exts) + if not path: + return None + if not allow_spaces: + return normalize_path(path) + return normsep(path) + + # The following snippet comes from `which` itself, with a slight + # modification to use lowercase extensions, because it's confusing rustup + # (on top of making results not really appealing to the eye). + + # Windows has the concept of a list of extensions (PATHEXT env var). + if sys.platform.startswith("win"): + exts = [e.lower() for e in environ.get("PATHEXT", "").split(pathsep)] + # If '.exe' is not in exts then obviously this is Win9x and + # or a bogus PATHEXT, then use a reasonable default. + if ".exe" not in exts: + exts = [".com", ".exe", ".bat"] + else: + exts = None + + if is_absolute_or_relative(file): + return which_normalize( + os.path.basename(file), path=os.path.dirname(file), exts=exts + ) + + if paths: + if not isinstance(paths, (list, tuple)): + die( + "Paths provided to find_program must be a list of strings, " "not %r", + paths, + ) + paths = pathsep.join(paths) + + return which_normalize(file, path=paths, exts=exts) + + +@imports("os") +@imports(_from="mozbuild.configure.util", _import="LineIO") +@imports(_from="tempfile", _import="mkstemp") +@imports(_import="subprocess") +def try_invoke_compiler( + configure_cache, compiler, language, source, flags=None, onerror=None, wrapper=[] +): + compiler_path = compiler[0] + compiler = wrapper + compiler + use_cache = configure_cache is not None + + if use_cache and compiler_path not in configure_cache.version_checked_compilers: + try: + version_info = subprocess.check_output( + [compiler_path, "--version"], + encoding="UTF-8", + ).strip() + except subprocess.CalledProcessError: + # There's no sane way to use the cache without the version details, so + # we need to avoid both reads from and writes to the cache. + use_cache = False + pass + + if use_cache: + if version_info != configure_cache.setdefault(compiler_path, {}).get( + "version" + ): + configure_cache[compiler_path].clear() + + configure_cache[compiler_path]["version"] = version_info + configure_cache.version_checked_compilers.add(compiler_path) + + flags = flags or [] + + if use_cache: + key = " ".join(compiler) + language + source + (" ".join(flags) or "") + + if key in configure_cache[compiler_path]: + return configure_cache[compiler_path][key] + + if not isinstance(flags, (list, tuple)): + die("Flags provided to try_compile must be a list of strings, " "not %r", flags) + + suffix = { + "C": ".c", + "C++": ".cpp", + }[language] + + fd, path = mkstemp(prefix="conftest.", suffix=suffix, text=True) + try: + source = source.encode("ascii", "replace") + + log.debug("Creating `%s` with content:", path) + with LineIO(lambda l: log.debug("| %s", l)) as out: + out.write(source) + + os.write(fd, source) + os.close(fd) + cmd = compiler + [path] + list(flags) + kwargs = {"onerror": onerror} + val = check_cmd_output(*cmd, **kwargs) + if use_cache: + configure_cache[compiler_path][key] = val + return val + finally: + os.remove(path) + + +def unique_list(l): + result = [] + for i in l: + if l not in result: + result.append(i) + return result + + +# Get values out of the Windows registry. This function can only be called on +# Windows. +# The `pattern` argument is a string starting with HKEY_ and giving the full +# "path" of the registry key to get the value for, with backslash separators. +# The string can contains wildcards ('*'). +# The result of this functions is an enumerator yielding tuples for each +# match. Each of these tuples contains the key name matching wildcards +# followed by the value. +# +# The `get_32_and_64_bit` argument is a boolean, if True then it will return the +# values from the 32-bit and 64-bit registry views. This defaults to False, +# which will return the view depending on the bitness of python. +# +# Examples: +# get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\' +# r'Windows Kits\Installed Roots\KitsRoot*') +# yields e.g.: +# ('KitsRoot81', r'C:\Program Files (x86)\Windows Kits\8.1\') +# ('KitsRoot10', r'C:\Program Files (x86)\Windows Kits\10\') +# +# get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\' +# r'Windows Kits\Installed Roots\KitsRoot8.1') +# yields e.g.: +# (r'C:\Program Files (x86)\Windows Kits\8.1\',) +# +# get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\' +# r'Windows Kits\Installed Roots\KitsRoot8.1', +# get_32_and_64_bit=True) +# yields e.g.: +# (r'C:\Program Files (x86)\Windows Kits\8.1\',) +# (r'C:\Program Files\Windows Kits\8.1\',) +# +# get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\' +# r'Windows Kits\*\KitsRoot*') +# yields e.g.: +# ('Installed Roots', 'KitsRoot81', +# r'C:\Program Files (x86)\Windows Kits\8.1\') +# ('Installed Roots', 'KitsRoot10', +# r'C:\Program Files (x86)\Windows Kits\10\') +# +# get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\' +# r'VisualStudio\VC\*\x86\*\Compiler') +# yields e.g.: +# ('19.0', 'arm', r'C:\...\amd64_arm\cl.exe') +# ('19.0', 'x64', r'C:\...\amd64\cl.exe') +# ('19.0', 'x86', r'C:\...\amd64_x86\cl.exe') +@imports(_import="winreg") +@imports(_from="__builtin__", _import="WindowsError") +@imports(_from="fnmatch", _import="fnmatch") +def get_registry_values(pattern, get_32_and_64_bit=False): + def enum_helper(func, key): + i = 0 + while True: + try: + yield func(key, i) + except WindowsError: + break + i += 1 + + def get_keys(key, pattern, access_mask): + try: + s = winreg.OpenKey(key, "\\".join(pattern[:-1]), 0, access_mask) + except WindowsError: + return + for k in enum_helper(winreg.EnumKey, s): + if fnmatch(k, pattern[-1]): + try: + yield k, winreg.OpenKey(s, k, 0, access_mask) + except WindowsError: + pass + + def get_values(key, pattern, access_mask): + try: + s = winreg.OpenKey(key, "\\".join(pattern[:-1]), 0, access_mask) + except WindowsError: + return + for k, v, t in enum_helper(winreg.EnumValue, s): + if fnmatch(k, pattern[-1]): + yield k, v + + def split_pattern(pattern): + subpattern = [] + for p in pattern: + subpattern.append(p) + if "*" in p: + yield subpattern + subpattern = [] + if subpattern: + yield subpattern + + def get_all_values(keys, pattern, access_mask): + for i, p in enumerate(pattern): + next_keys = [] + for base_key in keys: + matches = base_key[:-1] + base_key = base_key[-1] + if i == len(pattern) - 1: + want_name = "*" in p[-1] + for name, value in get_values(base_key, p, access_mask): + yield matches + ((name, value) if want_name else (value,)) + else: + for name, k in get_keys(base_key, p, access_mask): + next_keys.append(matches + (name, k)) + keys = next_keys + + pattern = pattern.split("\\") + assert pattern[0].startswith("HKEY_") + keys = [(getattr(winreg, pattern[0]),)] + pattern = list(split_pattern(pattern[1:])) + if get_32_and_64_bit: + for match in get_all_values( + keys, pattern, winreg.KEY_READ | winreg.KEY_WOW64_32KEY + ): + yield match + for match in get_all_values( + keys, pattern, winreg.KEY_READ | winreg.KEY_WOW64_64KEY + ): + yield match + else: + for match in get_all_values(keys, pattern, winreg.KEY_READ): + yield match + + +@imports(_from="mozbuild.configure.util", _import="Version", _as="_Version") +def Version(v): + "A version number that can be compared usefully." + return _Version(v) + + +# Denotes a deprecated option. Combines option() and @depends: +# @deprecated_option('--option') +# def option(value): +# ... +# @deprecated_option() takes the same arguments as option(), except `help`. +# The function may handle the option like a typical @depends function would, +# but it is recommended it emits a deprecation error message suggesting an +# alternative option to use if there is one. +@template +def deprecated_option(*args, **kwargs): + assert "help" not in kwargs + kwargs["help"] = "Deprecated" + opt = option(*args, **kwargs) + kwargs = {k: v for k, v in kwargs.items() if k == "when"} + + def decorator(func): + @depends(opt.option, **kwargs) + def deprecated(value): + if value.origin != "default": + return func(value) + + return deprecated + + return decorator + + +# Turn an object into an object that can be used as an argument to @depends. +# The given object can be a literal value, a function that takes no argument, +# or, for convenience, a @depends function. +@template +@imports(_from="mozbuild.configure", _import="SandboxDependsFunction") +def dependable(obj): + if isinstance(obj, SandboxDependsFunction): + return obj + return depends(when=True)(obj) + + +always = dependable(True) +never = dependable(False) + + +# Create a decorator that will only execute the body of a function +# if the passed function returns True when passed all positional +# arguments. +@template +def depends_tmpl(eval_args_fn, *args, **kwargs): + if kwargs: + assert len(kwargs) == 1 + when = kwargs["when"] + else: + when = None + + def decorator(func): + @depends(*args, when=when) + def wrapper(*args): + if eval_args_fn(args): + return func(*args) + + return wrapper + + return decorator + + +# Like @depends, but the decorated function is only called if one of the +# arguments it would be called with has a positive value (bool(value) is True) +@template +def depends_if(*args, **kwargs): + return depends_tmpl(any, *args, **kwargs) + + +# Like @depends, but the decorated function is only called if all of the +# arguments it would be called with have a positive value. +@template +def depends_all(*args, **kwargs): + return depends_tmpl(all, *args, **kwargs) + + +# A template providing a shorthand for setting a variable. The created +# option will only be settable with imply_option. +# It is expected that a project-specific moz.configure will call imply_option +# to set a value other than the default. +# If required, the set_as_define argument will additionally cause the variable +# to be set using set_define. +@template +def project_flag(env=None, set_as_define=False, **kwargs): + if not env: + configure_error("A project_flag must be passed a variable name to set.") + + opt = option(env=env, possible_origins=("implied",), **kwargs) + + @depends(opt.option) + def option_implementation(value): + if value: + if len(value): + return value + return bool(value) + + set_config(env, option_implementation) + if set_as_define: + set_define(env, option_implementation) + + +@template +@imports(_from="mozbuild.configure.constants", _import="RaiseErrorOnUse") +def obsolete_config(name, *, replacement): + set_config(name, RaiseErrorOnUse(f"{name} is obsolete. Use {replacement} instead.")) + + +# Hacks related to old-configure +# ============================== + + +@dependable +def old_configure_assignments(): + return [] + + +@template +def add_old_configure_assignment(var, value, when=None): + var = dependable(var) + value = dependable(value) + + @depends(old_configure_assignments, var, value, when=when) + @imports(_from="mozbuild.shellutil", _import="quote") + def add_assignment(assignments, var, value): + if var is None or value is None: + return + if value is True: + assignments.append((var, "1")) + elif value is False: + assignments.append((var, "")) + else: + if isinstance(value, (list, tuple)): + value = quote(*value) + assignments.append((var, str(value))) |