tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

util.configure (19715B)


      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")
      9 def 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")
     16 def 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("subprocess")
     27 @imports(_from="mozshellutil", _import="quote")
     28 @imports(_from="mozbuild.util", _import="system_encoding")
     29 def get_cmd_output(*args, **kwargs):
     30     log.debug("Executing: `%s`", quote(*args))
     31     proc = subprocess.Popen(
     32         args,
     33         stdout=subprocess.PIPE,
     34         stderr=subprocess.PIPE,
     35         # On Python 2 on Windows, close_fds prevents the process from inheriting
     36         # stdout/stderr. Elsewhere, it simply prevents it from inheriting extra
     37         # file descriptors, which is what we want.
     38         close_fds=os.name != "nt",
     39         encoding=system_encoding,
     40         errors="replace",
     41         **kwargs,
     42     )
     43     stdout, stderr = proc.communicate()
     44     return proc.wait(), stdout, stderr
     45 
     46 
     47 # A wrapper to obtain a process' output that returns the output generated
     48 # by running the given command if it exits normally, and streams that
     49 # output to log.debug and calls die or the given error callback if it
     50 # does not.
     51 @imports(_from="mozbuild.configure.util", _import="LineIO")
     52 @imports(_from="mozshellutil", _import="quote")
     53 def check_cmd_output(*args, **kwargs):
     54     onerror = kwargs.pop("onerror", None)
     55 
     56     with log.queue_debug():
     57         retcode, stdout, stderr = get_cmd_output(*args, **kwargs)
     58         if retcode == 0:
     59             with LineIO(lambda l: log.debug("| %s", l)) as o:
     60                 o.write(stderr)
     61             return stdout
     62 
     63         log.debug("The command returned non-zero exit status %d.", retcode)
     64         for out, desc in ((stdout, "output"), (stderr, "error output")):
     65             if out:
     66                 log.debug("Its %s was:", desc)
     67                 with LineIO(lambda l: log.debug("| %s", l)) as o:
     68                     o.write(out)
     69         if onerror:
     70             return onerror()
     71         die("Command `%s` failed with exit status %d." % (quote(*args), retcode))
     72 
     73 
     74 @imports("os")
     75 def is_absolute_or_relative(path):
     76     if os.altsep and os.altsep in path:
     77         return True
     78     return os.sep in path
     79 
     80 
     81 @imports(_import="mozpack.path", _as="mozpath")
     82 def normsep(path):
     83     return mozpath.normsep(path)
     84 
     85 
     86 @imports("ctypes")
     87 @imports(_from="ctypes", _import="wintypes")
     88 @imports(_from="mozbuild.configure.constants", _import="WindowsBinaryType")
     89 def windows_binary_type(path):
     90     """Obtain the type of a binary on Windows.
     91 
     92     Returns WindowsBinaryType constant.
     93     """
     94     GetBinaryTypeW = ctypes.windll.kernel32.GetBinaryTypeW
     95     GetBinaryTypeW.argtypes = [wintypes.LPWSTR, ctypes.POINTER(wintypes.DWORD)]
     96     GetBinaryTypeW.restype = wintypes.BOOL
     97 
     98     bin_type = wintypes.DWORD()
     99     res = GetBinaryTypeW(path, ctypes.byref(bin_type))
    100     if not res:
    101         die("could not obtain binary type of %s" % path)
    102 
    103     if bin_type.value == 0:
    104         return WindowsBinaryType("win32")
    105     elif bin_type.value == 6:
    106         return WindowsBinaryType("win64")
    107     # If we see another binary type, something is likely horribly wrong.
    108     else:
    109         die("unsupported binary type on %s: %s" % (path, bin_type))
    110 
    111 
    112 @imports("ctypes")
    113 @imports(_from="ctypes", _import="wintypes")
    114 def get_GetShortPathNameW():
    115     GetShortPathNameW = ctypes.windll.kernel32.GetShortPathNameW
    116     GetShortPathNameW.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD]
    117     GetShortPathNameW.restype = wintypes.DWORD
    118     return GetShortPathNameW
    119 
    120 
    121 @template
    122 @imports("ctypes")
    123 @imports("platform")
    124 @imports(_from="mozshellutil", _import="quote")
    125 def normalize_path():
    126     # Until the build system can properly handle programs that need quoting,
    127     # transform those paths into their short version on Windows (e.g.
    128     # c:\PROGRA~1...).
    129     if platform.system() == "Windows":
    130         GetShortPathNameW = get_GetShortPathNameW()
    131 
    132         def normalize_path(path):
    133             path = normsep(path)
    134             if quote(path) == path:
    135                 return path
    136             size = 0
    137             while True:
    138                 out = ctypes.create_unicode_buffer(size)
    139                 needed = GetShortPathNameW(path, out, size)
    140                 if size >= needed:
    141                     if " " in out.value:
    142                         die(
    143                             "GetShortPathName returned a long path name: `%s`. "
    144                             "Use `fsutil file setshortname' "
    145                             "to create a short name "
    146                             "for any components of this path "
    147                             "that have spaces.",
    148                             out.value,
    149                         )
    150                     return normsep(out.value)
    151                 size = needed
    152 
    153     else:
    154 
    155         def normalize_path(path):
    156             return normsep(path)
    157 
    158     return normalize_path
    159 
    160 
    161 normalize_path = normalize_path()
    162 
    163 
    164 @template
    165 @imports(_from="tempfile", _import="mkstemp")
    166 @imports("os")
    167 @imports(_from="contextlib", _import="contextmanager")
    168 @imports(_from="__builtin__", _import="FileNotFoundError")
    169 def make_create_temporary_file():
    170     @contextmanager
    171     def create_temporary_file(suffix):
    172         fd, path = mkstemp(prefix="conftest", suffix=suffix)
    173         os.close(fd)
    174         yield path
    175         try:
    176             os.remove(path)
    177         except FileNotFoundError:
    178             pass
    179 
    180     return create_temporary_file
    181 
    182 
    183 create_temporary_file = make_create_temporary_file()
    184 
    185 
    186 # Locates the given program using which, or returns the given path if it
    187 # exists.
    188 # The `paths` parameter may be passed to search the given paths instead of
    189 # $PATH.
    190 @imports("sys")
    191 @imports(_from="os", _import="pathsep")
    192 @imports(_from="os", _import="environ")
    193 @imports(_from="mozfile", _import="which")
    194 def find_program(file, paths=None, allow_spaces=False):
    195     def which_normalize(file, path, exts):
    196         path = which(file, path=path, exts=exts)
    197         if not path:
    198             return None
    199         if not allow_spaces:
    200             return normalize_path(path)
    201         return normsep(path)
    202 
    203     # The following snippet comes from `which` itself, with a slight
    204     # modification to use lowercase extensions, because it's confusing rustup
    205     # (on top of making results not really appealing to the eye).
    206 
    207     # Windows has the concept of a list of extensions (PATHEXT env var).
    208     if sys.platform.startswith("win"):
    209         exts = [e.lower() for e in environ.get("PATHEXT", "").split(pathsep)]
    210         # If '.exe' is not in exts then obviously this is Win9x and
    211         # or a bogus PATHEXT, then use a reasonable default.
    212         if ".exe" not in exts:
    213             exts = [".com", ".exe", ".bat"]
    214     else:
    215         exts = None
    216 
    217     if is_absolute_or_relative(file):
    218         return which_normalize(
    219             os.path.basename(file), path=os.path.dirname(file), exts=exts
    220         )
    221 
    222     if paths:
    223         if not isinstance(paths, (list, tuple)):
    224             die(
    225                 "Paths provided to find_program must be a list of strings, " "not %r",
    226                 paths,
    227             )
    228         paths = pathsep.join(paths)
    229 
    230     return which_normalize(file, path=paths, exts=exts)
    231 
    232 
    233 @imports("os")
    234 @imports(_from="mozbuild.configure.util", _import="LineIO")
    235 @imports(_from="__builtin__", _import="open")
    236 @imports(_import="subprocess")
    237 def try_invoke_compiler(
    238     configure_cache, compiler, language, source, flags=None, onerror=None, wrapper=[]
    239 ):
    240     compiler_path = compiler[0]
    241     compiler = wrapper + compiler
    242     use_cache = configure_cache is not None
    243 
    244     if use_cache and compiler_path not in configure_cache.version_checked_compilers:
    245         try:
    246             version_info = subprocess.check_output(
    247                 [compiler_path, "--version"],
    248                 encoding="UTF-8",
    249             ).strip()
    250         except subprocess.CalledProcessError:
    251             # There's no sane way to use the cache without the version details, so
    252             # we need to avoid both reads from and writes to the cache.
    253             use_cache = False
    254             pass
    255 
    256         if use_cache:
    257             if version_info != configure_cache.setdefault(compiler_path, {}).get(
    258                 "version"
    259             ):
    260                 configure_cache[compiler_path].clear()
    261 
    262             configure_cache[compiler_path]["version"] = version_info
    263             configure_cache.version_checked_compilers.add(compiler_path)
    264 
    265     flags = flags or []
    266 
    267     if use_cache:
    268         key = " ".join(compiler) + language + source + (" ".join(flags) or "")
    269 
    270         if key in configure_cache[compiler_path]:
    271             return configure_cache[compiler_path][key]
    272 
    273     if not isinstance(flags, (list, tuple)):
    274         die("Flags provided to try_compile must be a list of strings, " "not %r", flags)
    275 
    276     suffix = {
    277         "C": ".c",
    278         "C++": ".cpp",
    279     }[language]
    280 
    281     with create_temporary_file(suffix=suffix) as path:
    282         bsource = source.encode("ascii", "replace")
    283 
    284         log.debug("Creating `%s` with content:", path)
    285         with LineIO(lambda l: log.debug("| %s", l)) as out:
    286             out.write(bsource)
    287 
    288         with open(path, "w") as fd:
    289             fd.write(source)
    290 
    291         cmd = compiler + [path] + list(flags)
    292         kwargs = {"onerror": onerror}
    293         val = check_cmd_output(*cmd, **kwargs)
    294         if use_cache:
    295             configure_cache[compiler_path][key] = val
    296         return val
    297 
    298 
    299 def unique_list(l):
    300     result = []
    301     for i in l:
    302         if l not in result:
    303             result.append(i)
    304     return result
    305 
    306 
    307 # Get values out of the Windows registry. This function can only be called on
    308 # Windows.
    309 # The `pattern` argument is a string starting with HKEY_ and giving the full
    310 # "path" of the registry key to get the value for, with backslash separators.
    311 # The string can contains wildcards ('*').
    312 # The result of this functions is an enumerator yielding tuples for each
    313 # match. Each of these tuples contains the key name matching wildcards
    314 # followed by the value.
    315 #
    316 # The `get_32_and_64_bit` argument is a boolean, if True then it will return the
    317 # values from the 32-bit and 64-bit registry views. This defaults to False,
    318 # which will return the view depending on the bitness of python.
    319 #
    320 # Examples:
    321 #   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
    322 #                       r'Windows Kits\Installed Roots\KitsRoot*')
    323 #   yields e.g.:
    324 #     ('KitsRoot81', r'C:\Program Files (x86)\Windows Kits\8.1\')
    325 #     ('KitsRoot10', r'C:\Program Files (x86)\Windows Kits\10\')
    326 #
    327 #   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
    328 #                       r'Windows Kits\Installed Roots\KitsRoot8.1')
    329 #   yields e.g.:
    330 #     (r'C:\Program Files (x86)\Windows Kits\8.1\',)
    331 #
    332 #   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
    333 #                       r'Windows Kits\Installed Roots\KitsRoot8.1',
    334 #                       get_32_and_64_bit=True)
    335 #   yields e.g.:
    336 #     (r'C:\Program Files (x86)\Windows Kits\8.1\',)
    337 #     (r'C:\Program Files\Windows Kits\8.1\',)
    338 #
    339 #   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
    340 #                       r'Windows Kits\*\KitsRoot*')
    341 #   yields e.g.:
    342 #     ('Installed Roots', 'KitsRoot81',
    343 #      r'C:\Program Files (x86)\Windows Kits\8.1\')
    344 #     ('Installed Roots', 'KitsRoot10',
    345 #      r'C:\Program Files (x86)\Windows Kits\10\')
    346 #
    347 #   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
    348 #                       r'VisualStudio\VC\*\x86\*\Compiler')
    349 #   yields e.g.:
    350 #     ('19.0', 'arm', r'C:\...\amd64_arm\cl.exe')
    351 #     ('19.0', 'x64', r'C:\...\amd64\cl.exe')
    352 #     ('19.0', 'x86', r'C:\...\amd64_x86\cl.exe')
    353 @imports(_import="winreg")
    354 @imports(_from="__builtin__", _import="WindowsError")
    355 @imports(_from="fnmatch", _import="fnmatch")
    356 def get_registry_values(pattern, get_32_and_64_bit=False):
    357     def enum_helper(func, key):
    358         i = 0
    359         while True:
    360             try:
    361                 yield func(key, i)
    362             except WindowsError:
    363                 break
    364             i += 1
    365 
    366     def get_keys(key, pattern, access_mask):
    367         try:
    368             s = winreg.OpenKey(key, "\\".join(pattern[:-1]), 0, access_mask)
    369         except WindowsError:
    370             return
    371         for k in enum_helper(winreg.EnumKey, s):
    372             if fnmatch(k, pattern[-1]):
    373                 try:
    374                     yield k, winreg.OpenKey(s, k, 0, access_mask)
    375                 except WindowsError:
    376                     pass
    377 
    378     def get_values(key, pattern, access_mask):
    379         try:
    380             s = winreg.OpenKey(key, "\\".join(pattern[:-1]), 0, access_mask)
    381         except WindowsError:
    382             return
    383         for k, v, t in enum_helper(winreg.EnumValue, s):
    384             if fnmatch(k, pattern[-1]):
    385                 yield k, v
    386 
    387     def split_pattern(pattern):
    388         subpattern = []
    389         for p in pattern:
    390             subpattern.append(p)
    391             if "*" in p:
    392                 yield subpattern
    393                 subpattern = []
    394         if subpattern:
    395             yield subpattern
    396 
    397     def get_all_values(keys, pattern, access_mask):
    398         for i, p in enumerate(pattern):
    399             next_keys = []
    400             for base_key in keys:
    401                 matches = base_key[:-1]
    402                 base_key = base_key[-1]
    403                 if i == len(pattern) - 1:
    404                     want_name = "*" in p[-1]
    405                     for name, value in get_values(base_key, p, access_mask):
    406                         yield matches + ((name, value) if want_name else (value,))
    407                 else:
    408                     for name, k in get_keys(base_key, p, access_mask):
    409                         next_keys.append(matches + (name, k))
    410             keys = next_keys
    411 
    412     pattern = pattern.split("\\")
    413     assert pattern[0].startswith("HKEY_")
    414     keys = [(getattr(winreg, pattern[0]),)]
    415     pattern = list(split_pattern(pattern[1:]))
    416     if get_32_and_64_bit:
    417         for match in get_all_values(
    418             keys, pattern, winreg.KEY_READ | winreg.KEY_WOW64_32KEY
    419         ):
    420             yield match
    421         for match in get_all_values(
    422             keys, pattern, winreg.KEY_READ | winreg.KEY_WOW64_64KEY
    423         ):
    424             yield match
    425     else:
    426         for match in get_all_values(keys, pattern, winreg.KEY_READ):
    427             yield match
    428 
    429 
    430 @imports(_from="mozbuild.configure.util", _import="Version", _as="_Version")
    431 def Version(v):
    432     "A version number that can be compared usefully."
    433     return _Version(v)
    434 
    435 
    436 # Denotes a deprecated option. Combines option() and @depends:
    437 # @deprecated_option('--option')
    438 # def option(value):
    439 #     ...
    440 # @deprecated_option() takes the same arguments as option(), except `help`.
    441 # The function may handle the option like a typical @depends function would,
    442 # but it is recommended it emits a deprecation error message suggesting an
    443 # alternative option to use if there is one.
    444 @template
    445 def deprecated_option(*args, **kwargs):
    446     assert "help" not in kwargs
    447     kwargs["help"] = "Deprecated"
    448     opt = option(*args, **kwargs)
    449     kwargs = {k: v for k, v in kwargs.items() if k == "when"}
    450 
    451     def decorator(func):
    452         @depends(opt.option, **kwargs)
    453         def deprecated(value):
    454             if value.origin != "default":
    455                 return func(value)
    456 
    457         return deprecated
    458 
    459     return decorator
    460 
    461 
    462 # Turn an object into an object that can be used as an argument to @depends.
    463 # The given object can be a literal value, a function that takes no argument,
    464 # or, for convenience, a @depends function.
    465 @template
    466 @imports(_from="mozbuild.configure", _import="SandboxDependsFunction")
    467 def dependable(obj):
    468     if isinstance(obj, SandboxDependsFunction):
    469         return obj
    470     return depends(when=True)(obj)
    471 
    472 
    473 always = dependable(True)
    474 never = dependable(False)
    475 
    476 
    477 # Create a decorator that will only execute the body of a function
    478 # if the passed function returns True when passed all positional
    479 # arguments.
    480 @template
    481 def depends_tmpl(eval_args_fn, *args, **kwargs):
    482     if kwargs:
    483         assert len(kwargs) == 1
    484         when = kwargs["when"]
    485     else:
    486         when = None
    487 
    488     def decorator(func):
    489         @depends(*args, when=when)
    490         def wrapper(*args):
    491             if eval_args_fn(args):
    492                 return func(*args)
    493 
    494         return wrapper
    495 
    496     return decorator
    497 
    498 
    499 # Like @depends, but the decorated function is only called if one of the
    500 # arguments it would be called with has a positive value (bool(value) is True)
    501 @template
    502 def depends_if(*args, **kwargs):
    503     return depends_tmpl(any, *args, **kwargs)
    504 
    505 
    506 # Like @depends, but the decorated function is only called if all of the
    507 # arguments it would be called with have a positive value.
    508 @template
    509 def depends_all(*args, **kwargs):
    510     return depends_tmpl(all, *args, **kwargs)
    511 
    512 
    513 # A template providing a shorthand for setting a variable. The created
    514 # option will only be settable with imply_option.
    515 # It is expected that a project-specific moz.configure will call imply_option
    516 # to set a value other than the default.
    517 # If required, the set_as_define argument will additionally cause the variable
    518 # to be set using set_define.
    519 @template
    520 def project_flag(env=None, set_as_define=False, **kwargs):
    521     if not env:
    522         configure_error("A project_flag must be passed a variable name to set.")
    523 
    524     if kwargs.get("nargs", 0) not in (0, 1):
    525         configure_error("A project_flag must be passed nargs={0,1}.")
    526 
    527     opt = option(env=env, possible_origins=("implied",), **kwargs)
    528 
    529     @depends(opt.option)
    530     def option_implementation(value):
    531         if value:
    532             if len(value) == 1:
    533                 return value[0]
    534             elif len(value):
    535                 return value
    536             return bool(value)
    537 
    538     set_config(env, option_implementation)
    539     if set_as_define:
    540         set_define(env, option_implementation)
    541 
    542 
    543 # A template providing a shorthand for setting a variable. The created
    544 # option will only be settable from a confvars.sh file.
    545 # If required, the set_as_define argument will additionally cause the variable
    546 # to be set using set_define.
    547 # Similarly, set_as_config can be set to False if the variable should not be
    548 # passed to set_config.
    549 @template
    550 def confvar(
    551     env=None,
    552     set_as_config=True,
    553     set_as_define=False,
    554     allow_implied=False,
    555     **kwargs,
    556 ):
    557     if not env:
    558         configure_error("A project_flag must be passed a variable name to set.")
    559 
    560     if kwargs.get("nargs", 0) not in (0, 1):
    561         configure_error("A project_flag must be passed nargs={0,1}.")
    562 
    563     origins = ("confvars",)
    564     if allow_implied:
    565         origins += ("implied",)
    566     opt = option(env=env, possible_origins=origins, **kwargs)
    567 
    568     @depends(opt.option)
    569     def option_implementation(value):
    570         if value:
    571             if len(value) == 1:
    572                 return value[0]
    573             elif len(value):
    574                 return value
    575             return bool(value)
    576 
    577     if set_as_config:
    578         set_config(env, option_implementation)
    579     if set_as_define:
    580         set_define(env, option_implementation)
    581 
    582 
    583 @template
    584 @imports(_from="mozbuild.configure.constants", _import="RaiseErrorOnUse")
    585 def obsolete_config(name, *, replacement):
    586     set_config(name, RaiseErrorOnUse(f"{name} is obsolete. Use {replacement} instead."))