Bug 1904139 - Don't re-initialize platform font list from GetDefaultFont. r=jfkthame
[gecko.git] / build / moz.configure / util.configure
blob7330c347ab7c55f75eab84465779c68394aa7bee
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/.
8 @imports("sys")
9 def die(*args):
10     "Print an error and terminate configure."
11     log.error(*args)
12     sys.exit(1)
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)
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="mozbuild.shellutil", _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
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="mozbuild.shellutil", _import="quote")
53 def check_cmd_output(*args, **kwargs):
54     onerror = kwargs.pop("onerror", None)
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
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))
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
81 @imports(_import="mozpack.path", _as="mozpath")
82 def normsep(path):
83     return mozpath.normsep(path)
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.
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
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)
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))
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
121 @template
122 @imports("ctypes")
123 @imports("platform")
124 @imports(_from="mozbuild.shellutil", _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()
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
153     else:
155         def normalize_path(path):
156             return normsep(path)
158     return normalize_path
161 normalize_path = normalize_path()
164 # Locates the given program using which, or returns the given path if it
165 # exists.
166 # The `paths` parameter may be passed to search the given paths instead of
167 # $PATH.
168 @imports("sys")
169 @imports(_from="os", _import="pathsep")
170 @imports(_from="os", _import="environ")
171 @imports(_from="mozfile", _import="which")
172 def find_program(file, paths=None, allow_spaces=False):
173     def which_normalize(file, path, exts):
174         path = which(file, path=path, exts=exts)
175         if not path:
176             return None
177         if not allow_spaces:
178             return normalize_path(path)
179         return normsep(path)
181     # The following snippet comes from `which` itself, with a slight
182     # modification to use lowercase extensions, because it's confusing rustup
183     # (on top of making results not really appealing to the eye).
185     # Windows has the concept of a list of extensions (PATHEXT env var).
186     if sys.platform.startswith("win"):
187         exts = [e.lower() for e in environ.get("PATHEXT", "").split(pathsep)]
188         # If '.exe' is not in exts then obviously this is Win9x and
189         # or a bogus PATHEXT, then use a reasonable default.
190         if ".exe" not in exts:
191             exts = [".com", ".exe", ".bat"]
192     else:
193         exts = None
195     if is_absolute_or_relative(file):
196         return which_normalize(
197             os.path.basename(file), path=os.path.dirname(file), exts=exts
198         )
200     if paths:
201         if not isinstance(paths, (list, tuple)):
202             die(
203                 "Paths provided to find_program must be a list of strings, " "not %r",
204                 paths,
205             )
206         paths = pathsep.join(paths)
208     return which_normalize(file, path=paths, exts=exts)
211 @imports("os")
212 @imports(_from="mozbuild.configure.util", _import="LineIO")
213 @imports(_from="tempfile", _import="mkstemp")
214 @imports(_import="subprocess")
215 def try_invoke_compiler(
216     configure_cache, compiler, language, source, flags=None, onerror=None, wrapper=[]
218     compiler_path = compiler[0]
219     compiler = wrapper + compiler
220     use_cache = configure_cache is not None
222     if use_cache and compiler_path not in configure_cache.version_checked_compilers:
223         try:
224             version_info = subprocess.check_output(
225                 [compiler_path, "--version"],
226                 encoding="UTF-8",
227             ).strip()
228         except subprocess.CalledProcessError:
229             # There's no sane way to use the cache without the version details, so
230             # we need to avoid both reads from and writes to the cache.
231             use_cache = False
232             pass
234         if use_cache:
235             if version_info != configure_cache.setdefault(compiler_path, {}).get(
236                 "version"
237             ):
238                 configure_cache[compiler_path].clear()
240             configure_cache[compiler_path]["version"] = version_info
241             configure_cache.version_checked_compilers.add(compiler_path)
243     flags = flags or []
245     if use_cache:
246         key = " ".join(compiler) + language + source + (" ".join(flags) or "")
248         if key in configure_cache[compiler_path]:
249             return configure_cache[compiler_path][key]
251     if not isinstance(flags, (list, tuple)):
252         die("Flags provided to try_compile must be a list of strings, " "not %r", flags)
254     suffix = {
255         "C": ".c",
256         "C++": ".cpp",
257     }[language]
259     fd, path = mkstemp(prefix="conftest.", suffix=suffix, text=True)
260     try:
261         source = source.encode("ascii", "replace")
263         log.debug("Creating `%s` with content:", path)
264         with LineIO(lambda l: log.debug("| %s", l)) as out:
265             out.write(source)
267         os.write(fd, source)
268         os.close(fd)
269         cmd = compiler + [path] + list(flags)
270         kwargs = {"onerror": onerror}
271         val = check_cmd_output(*cmd, **kwargs)
272         if use_cache:
273             configure_cache[compiler_path][key] = val
274         return val
275     finally:
276         os.remove(path)
279 def unique_list(l):
280     result = []
281     for i in l:
282         if l not in result:
283             result.append(i)
284     return result
287 # Get values out of the Windows registry. This function can only be called on
288 # Windows.
289 # The `pattern` argument is a string starting with HKEY_ and giving the full
290 # "path" of the registry key to get the value for, with backslash separators.
291 # The string can contains wildcards ('*').
292 # The result of this functions is an enumerator yielding tuples for each
293 # match. Each of these tuples contains the key name matching wildcards
294 # followed by the value.
296 # The `get_32_and_64_bit` argument is a boolean, if True then it will return the
297 # values from the 32-bit and 64-bit registry views. This defaults to False,
298 # which will return the view depending on the bitness of python.
300 # Examples:
301 #   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
302 #                       r'Windows Kits\Installed Roots\KitsRoot*')
303 #   yields e.g.:
304 #     ('KitsRoot81', r'C:\Program Files (x86)\Windows Kits\8.1\')
305 #     ('KitsRoot10', r'C:\Program Files (x86)\Windows Kits\10\')
307 #   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
308 #                       r'Windows Kits\Installed Roots\KitsRoot8.1')
309 #   yields e.g.:
310 #     (r'C:\Program Files (x86)\Windows Kits\8.1\',)
312 #   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
313 #                       r'Windows Kits\Installed Roots\KitsRoot8.1',
314 #                       get_32_and_64_bit=True)
315 #   yields e.g.:
316 #     (r'C:\Program Files (x86)\Windows Kits\8.1\',)
317 #     (r'C:\Program Files\Windows Kits\8.1\',)
319 #   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
320 #                       r'Windows Kits\*\KitsRoot*')
321 #   yields e.g.:
322 #     ('Installed Roots', 'KitsRoot81',
323 #      r'C:\Program Files (x86)\Windows Kits\8.1\')
324 #     ('Installed Roots', 'KitsRoot10',
325 #      r'C:\Program Files (x86)\Windows Kits\10\')
327 #   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
328 #                       r'VisualStudio\VC\*\x86\*\Compiler')
329 #   yields e.g.:
330 #     ('19.0', 'arm', r'C:\...\amd64_arm\cl.exe')
331 #     ('19.0', 'x64', r'C:\...\amd64\cl.exe')
332 #     ('19.0', 'x86', r'C:\...\amd64_x86\cl.exe')
333 @imports(_import="winreg")
334 @imports(_from="__builtin__", _import="WindowsError")
335 @imports(_from="fnmatch", _import="fnmatch")
336 def get_registry_values(pattern, get_32_and_64_bit=False):
337     def enum_helper(func, key):
338         i = 0
339         while True:
340             try:
341                 yield func(key, i)
342             except WindowsError:
343                 break
344             i += 1
346     def get_keys(key, pattern, access_mask):
347         try:
348             s = winreg.OpenKey(key, "\\".join(pattern[:-1]), 0, access_mask)
349         except WindowsError:
350             return
351         for k in enum_helper(winreg.EnumKey, s):
352             if fnmatch(k, pattern[-1]):
353                 try:
354                     yield k, winreg.OpenKey(s, k, 0, access_mask)
355                 except WindowsError:
356                     pass
358     def get_values(key, pattern, access_mask):
359         try:
360             s = winreg.OpenKey(key, "\\".join(pattern[:-1]), 0, access_mask)
361         except WindowsError:
362             return
363         for k, v, t in enum_helper(winreg.EnumValue, s):
364             if fnmatch(k, pattern[-1]):
365                 yield k, v
367     def split_pattern(pattern):
368         subpattern = []
369         for p in pattern:
370             subpattern.append(p)
371             if "*" in p:
372                 yield subpattern
373                 subpattern = []
374         if subpattern:
375             yield subpattern
377     def get_all_values(keys, pattern, access_mask):
378         for i, p in enumerate(pattern):
379             next_keys = []
380             for base_key in keys:
381                 matches = base_key[:-1]
382                 base_key = base_key[-1]
383                 if i == len(pattern) - 1:
384                     want_name = "*" in p[-1]
385                     for name, value in get_values(base_key, p, access_mask):
386                         yield matches + ((name, value) if want_name else (value,))
387                 else:
388                     for name, k in get_keys(base_key, p, access_mask):
389                         next_keys.append(matches + (name, k))
390             keys = next_keys
392     pattern = pattern.split("\\")
393     assert pattern[0].startswith("HKEY_")
394     keys = [(getattr(winreg, pattern[0]),)]
395     pattern = list(split_pattern(pattern[1:]))
396     if get_32_and_64_bit:
397         for match in get_all_values(
398             keys, pattern, winreg.KEY_READ | winreg.KEY_WOW64_32KEY
399         ):
400             yield match
401         for match in get_all_values(
402             keys, pattern, winreg.KEY_READ | winreg.KEY_WOW64_64KEY
403         ):
404             yield match
405     else:
406         for match in get_all_values(keys, pattern, winreg.KEY_READ):
407             yield match
410 @imports(_from="mozbuild.configure.util", _import="Version", _as="_Version")
411 def Version(v):
412     "A version number that can be compared usefully."
413     return _Version(v)
416 # Denotes a deprecated option. Combines option() and @depends:
417 # @deprecated_option('--option')
418 # def option(value):
419 #     ...
420 # @deprecated_option() takes the same arguments as option(), except `help`.
421 # The function may handle the option like a typical @depends function would,
422 # but it is recommended it emits a deprecation error message suggesting an
423 # alternative option to use if there is one.
424 @template
425 def deprecated_option(*args, **kwargs):
426     assert "help" not in kwargs
427     kwargs["help"] = "Deprecated"
428     opt = option(*args, **kwargs)
429     kwargs = {k: v for k, v in kwargs.items() if k == "when"}
431     def decorator(func):
432         @depends(opt.option, **kwargs)
433         def deprecated(value):
434             if value.origin != "default":
435                 return func(value)
437         return deprecated
439     return decorator
442 # Turn an object into an object that can be used as an argument to @depends.
443 # The given object can be a literal value, a function that takes no argument,
444 # or, for convenience, a @depends function.
445 @template
446 @imports(_from="mozbuild.configure", _import="SandboxDependsFunction")
447 def dependable(obj):
448     if isinstance(obj, SandboxDependsFunction):
449         return obj
450     return depends(when=True)(obj)
453 always = dependable(True)
454 never = dependable(False)
457 # Create a decorator that will only execute the body of a function
458 # if the passed function returns True when passed all positional
459 # arguments.
460 @template
461 def depends_tmpl(eval_args_fn, *args, **kwargs):
462     if kwargs:
463         assert len(kwargs) == 1
464         when = kwargs["when"]
465     else:
466         when = None
468     def decorator(func):
469         @depends(*args, when=when)
470         def wrapper(*args):
471             if eval_args_fn(args):
472                 return func(*args)
474         return wrapper
476     return decorator
479 # Like @depends, but the decorated function is only called if one of the
480 # arguments it would be called with has a positive value (bool(value) is True)
481 @template
482 def depends_if(*args, **kwargs):
483     return depends_tmpl(any, *args, **kwargs)
486 # Like @depends, but the decorated function is only called if all of the
487 # arguments it would be called with have a positive value.
488 @template
489 def depends_all(*args, **kwargs):
490     return depends_tmpl(all, *args, **kwargs)
493 # A template providing a shorthand for setting a variable. The created
494 # option will only be settable with imply_option.
495 # It is expected that a project-specific moz.configure will call imply_option
496 # to set a value other than the default.
497 # If required, the set_as_define argument will additionally cause the variable
498 # to be set using set_define.
499 @template
500 def project_flag(env=None, set_as_define=False, **kwargs):
501     if not env:
502         configure_error("A project_flag must be passed a variable name to set.")
504     if kwargs.get("nargs", 0) not in (0, 1):
505         configure_error("A project_flag must be passed nargs={0,1}.")
507     opt = option(env=env, possible_origins=("implied",), **kwargs)
509     @depends(opt.option)
510     def option_implementation(value):
511         if value:
512             if len(value) == 1:
513                 return value[0]
514             elif len(value):
515                 return value
516             return bool(value)
518     set_config(env, option_implementation)
519     if set_as_define:
520         set_define(env, option_implementation)
523 @template
524 @imports(_from="mozbuild.configure.constants", _import="RaiseErrorOnUse")
525 def obsolete_config(name, *, replacement):
526     set_config(name, RaiseErrorOnUse(f"{name} is obsolete. Use {replacement} instead."))
529 # Hacks related to old-configure
530 # ==============================
533 @dependable
534 def old_configure_assignments():
535     return []
538 @template
539 def add_old_configure_assignment(var, value, when=None):
540     var = dependable(var)
541     value = dependable(value)
543     @depends(old_configure_assignments, var, value, when=when)
544     @imports(_from="mozbuild.shellutil", _import="quote")
545     def add_assignment(assignments, var, value):
546         if var is None or value is None:
547             return
548         if value is True:
549             assignments.append((var, "1"))
550         elif value is False:
551             assignments.append((var, ""))
552         else:
553             if isinstance(value, (list, tuple)):
554                 value = quote(*value)
555             assignments.append((var, str(value)))