gecko-dev/build/moz.configure/util.configure

576 lines
19 KiB
Python

# -*- 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()
@template
@imports(_from="tempfile", _import="mkstemp")
@imports("os")
@imports(_from="contextlib", _import="contextmanager")
@imports(_from="__builtin__", _import="FileNotFoundError")
def make_create_temporary_file():
@contextmanager
def create_temporary_file(suffix):
fd, path = mkstemp(prefix="conftest", suffix=suffix)
os.close(fd)
yield path
try:
os.remove(path)
except FileNotFoundError:
pass
return create_temporary_file
create_temporary_file = make_create_temporary_file()
# 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="__builtin__", _import="open")
@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]
with create_temporary_file(suffix=suffix) as path:
bsource = source.encode("ascii", "replace")
log.debug("Creating `%s` with content:", path)
with LineIO(lambda l: log.debug("| %s", l)) as out:
out.write(bsource)
with open(path, "w") as fd:
fd.write(source)
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
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.")
if kwargs.get("nargs", 0) not in (0, 1):
configure_error("A project_flag must be passed nargs={0,1}.")
opt = option(env=env, possible_origins=("implied",), **kwargs)
@depends(opt.option)
def option_implementation(value):
if value:
if len(value) == 1:
return value[0]
elif 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)))