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."))