1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5from __future__ import absolute_import, print_function, unicode_literals 6 7import json 8import logging 9import mozpack.path as mozpath 10import multiprocessing 11import os 12import subprocess 13import sys 14import which 15 16from mach.mixin.process import ProcessExecutionMixin 17from mozversioncontrol import ( 18 get_repository_from_build_config, 19 get_repository_object, 20 InvalidRepoPath, 21) 22 23from .backend.configenvironment import ConfigEnvironment 24from .controller.clobber import Clobberer 25from .mozconfig import ( 26 MozconfigFindException, 27 MozconfigLoadException, 28 MozconfigLoader, 29) 30from .pythonutil import find_python3_executable 31from .util import memoized_property 32from .virtualenv import VirtualenvManager 33 34 35_config_guess_output = [] 36 37 38def ancestors(path): 39 """Emit the parent directories of a path.""" 40 while path: 41 yield path 42 newpath = os.path.dirname(path) 43 if newpath == path: 44 break 45 path = newpath 46 47def samepath(path1, path2): 48 if hasattr(os.path, 'samefile'): 49 return os.path.samefile(path1, path2) 50 return os.path.normcase(os.path.realpath(path1)) == \ 51 os.path.normcase(os.path.realpath(path2)) 52 53class BadEnvironmentException(Exception): 54 """Base class for errors raised when the build environment is not sane.""" 55 56 57class BuildEnvironmentNotFoundException(BadEnvironmentException): 58 """Raised when we could not find a build environment.""" 59 60 61class ObjdirMismatchException(BadEnvironmentException): 62 """Raised when the current dir is an objdir and doesn't match the mozconfig.""" 63 def __init__(self, objdir1, objdir2): 64 self.objdir1 = objdir1 65 self.objdir2 = objdir2 66 67 def __str__(self): 68 return "Objdir mismatch: %s != %s" % (self.objdir1, self.objdir2) 69 70 71class MozbuildObject(ProcessExecutionMixin): 72 """Base class providing basic functionality useful to many modules. 73 74 Modules in this package typically require common functionality such as 75 accessing the current config, getting the location of the source directory, 76 running processes, etc. This classes provides that functionality. Other 77 modules can inherit from this class to obtain this functionality easily. 78 """ 79 def __init__(self, topsrcdir, settings, log_manager, topobjdir=None, 80 mozconfig=MozconfigLoader.AUTODETECT): 81 """Create a new Mozbuild object instance. 82 83 Instances are bound to a source directory, a ConfigSettings instance, 84 and a LogManager instance. The topobjdir may be passed in as well. If 85 it isn't, it will be calculated from the active mozconfig. 86 """ 87 self.topsrcdir = mozpath.normsep(topsrcdir) 88 self.settings = settings 89 90 self.populate_logger() 91 self.log_manager = log_manager 92 93 self._make = None 94 self._topobjdir = mozpath.normsep(topobjdir) if topobjdir else topobjdir 95 self._mozconfig = mozconfig 96 self._config_environment = None 97 self._virtualenv_manager = None 98 99 @classmethod 100 def from_environment(cls, cwd=None, detect_virtualenv_mozinfo=True): 101 """Create a MozbuildObject by detecting the proper one from the env. 102 103 This examines environment state like the current working directory and 104 creates a MozbuildObject from the found source directory, mozconfig, etc. 105 106 The role of this function is to identify a topsrcdir, topobjdir, and 107 mozconfig file. 108 109 If the current working directory is inside a known objdir, we always 110 use the topsrcdir and mozconfig associated with that objdir. 111 112 If the current working directory is inside a known srcdir, we use that 113 topsrcdir and look for mozconfigs using the default mechanism, which 114 looks inside environment variables. 115 116 If the current Python interpreter is running from a virtualenv inside 117 an objdir, we use that as our objdir. 118 119 If we're not inside a srcdir or objdir, an exception is raised. 120 121 detect_virtualenv_mozinfo determines whether we should look for a 122 mozinfo.json file relative to the virtualenv directory. This was 123 added to facilitate testing. Callers likely shouldn't change the 124 default. 125 """ 126 127 cwd = cwd or os.getcwd() 128 topsrcdir = None 129 topobjdir = None 130 mozconfig = MozconfigLoader.AUTODETECT 131 132 def load_mozinfo(path): 133 info = json.load(open(path, 'rt')) 134 topsrcdir = info.get('topsrcdir') 135 topobjdir = os.path.dirname(path) 136 mozconfig = info.get('mozconfig') 137 return topsrcdir, topobjdir, mozconfig 138 139 for dir_path in ancestors(cwd): 140 # If we find a mozinfo.json, we are in the objdir. 141 mozinfo_path = os.path.join(dir_path, 'mozinfo.json') 142 if os.path.isfile(mozinfo_path): 143 topsrcdir, topobjdir, mozconfig = load_mozinfo(mozinfo_path) 144 break 145 146 # We choose an arbitrary file as an indicator that this is a 147 # srcdir. We go with ourself because why not! 148 our_path = os.path.join(dir_path, 'python', 'mozbuild', 'mozbuild', 'base.py') 149 if os.path.isfile(our_path): 150 topsrcdir = dir_path 151 break 152 153 # See if we're running from a Python virtualenv that's inside an objdir. 154 mozinfo_path = os.path.join(os.path.dirname(sys.prefix), "mozinfo.json") 155 if detect_virtualenv_mozinfo and os.path.isfile(mozinfo_path): 156 topsrcdir, topobjdir, mozconfig = load_mozinfo(mozinfo_path) 157 158 # If we were successful, we're only guaranteed to find a topsrcdir. If 159 # we couldn't find that, there's nothing we can do. 160 if not topsrcdir: 161 raise BuildEnvironmentNotFoundException( 162 'Could not find Mozilla source tree or build environment.') 163 164 topsrcdir = mozpath.normsep(topsrcdir) 165 if topobjdir: 166 topobjdir = mozpath.normsep(os.path.normpath(topobjdir)) 167 168 if topsrcdir == topobjdir: 169 raise BadEnvironmentException('The object directory appears ' 170 'to be the same as your source directory (%s). This build ' 171 'configuration is not supported.' % topsrcdir) 172 173 # If we can't resolve topobjdir, oh well. We'll figure out when we need 174 # one. 175 return cls(topsrcdir, None, None, topobjdir=topobjdir, 176 mozconfig=mozconfig) 177 178 def resolve_mozconfig_topobjdir(self, default=None): 179 topobjdir = self.mozconfig['topobjdir'] or default 180 if not topobjdir: 181 return None 182 183 if '@CONFIG_GUESS@' in topobjdir: 184 topobjdir = topobjdir.replace('@CONFIG_GUESS@', 185 self.resolve_config_guess()) 186 187 if not os.path.isabs(topobjdir): 188 topobjdir = os.path.abspath(os.path.join(self.topsrcdir, topobjdir)) 189 190 return mozpath.normsep(os.path.normpath(topobjdir)) 191 192 @property 193 def topobjdir(self): 194 if self._topobjdir is None: 195 self._topobjdir = self.resolve_mozconfig_topobjdir( 196 default='obj-@CONFIG_GUESS@') 197 198 return self._topobjdir 199 200 @property 201 def virtualenv_manager(self): 202 if self._virtualenv_manager is None: 203 self._virtualenv_manager = VirtualenvManager(self.topsrcdir, 204 self.topobjdir, os.path.join(self.topobjdir, '_virtualenv'), 205 sys.stdout, os.path.join(self.topsrcdir, 'build', 206 'virtualenv_packages.txt')) 207 208 return self._virtualenv_manager 209 210 @property 211 def mozconfig(self): 212 """Returns information about the current mozconfig file. 213 214 This a dict as returned by MozconfigLoader.read_mozconfig() 215 """ 216 if not isinstance(self._mozconfig, dict): 217 loader = MozconfigLoader(self.topsrcdir) 218 self._mozconfig = loader.read_mozconfig(path=self._mozconfig) 219 220 return self._mozconfig 221 222 @property 223 def config_environment(self): 224 """Returns the ConfigEnvironment for the current build configuration. 225 226 This property is only available once configure has executed. 227 228 If configure's output is not available, this will raise. 229 """ 230 if self._config_environment: 231 return self._config_environment 232 233 config_status = os.path.join(self.topobjdir, 'config.status') 234 235 if not os.path.exists(config_status): 236 raise BuildEnvironmentNotFoundException('config.status not available. Run configure.') 237 238 self._config_environment = \ 239 ConfigEnvironment.from_config_status(config_status) 240 241 return self._config_environment 242 243 @property 244 def defines(self): 245 return self.config_environment.defines 246 247 @property 248 def non_global_defines(self): 249 return self.config_environment.non_global_defines 250 251 @property 252 def substs(self): 253 return self.config_environment.substs 254 255 @property 256 def distdir(self): 257 return os.path.join(self.topobjdir, 'dist') 258 259 @property 260 def bindir(self): 261 return os.path.join(self.topobjdir, 'dist', 'bin') 262 263 @property 264 def includedir(self): 265 return os.path.join(self.topobjdir, 'dist', 'include') 266 267 @property 268 def statedir(self): 269 return os.path.join(self.topobjdir, '.mozbuild') 270 271 @property 272 def platform(self): 273 """Returns current platform and architecture name""" 274 import mozinfo 275 platform_name = None 276 bits = str(mozinfo.info['bits']) 277 if mozinfo.isLinux: 278 platform_name = "linux" + bits 279 elif mozinfo.isWin: 280 platform_name = "win" + bits 281 elif mozinfo.isMac: 282 platform_name = "macosx" + bits 283 284 return platform_name, bits + 'bit' 285 286 @memoized_property 287 def extra_environment_variables(self): 288 '''Some extra environment variables are stored in .mozconfig.mk. 289 This functions extracts and returns them.''' 290 from mozbuild import shellutil 291 mozconfig_mk = os.path.join(self.topobjdir, '.mozconfig.mk') 292 env = {} 293 with open(mozconfig_mk) as fh: 294 for line in fh: 295 if line.startswith('export '): 296 exports = shellutil.split(line)[1:] 297 for e in exports: 298 if '=' in e: 299 key, value = e.split('=') 300 env[key] = value 301 return env 302 303 @memoized_property 304 def repository(self): 305 '''Get a `mozversioncontrol.Repository` object for the 306 top source directory.''' 307 # We try to obtain a repo using the configured VCS info first. 308 # If we don't have a configure context, fall back to auto-detection. 309 try: 310 return get_repository_from_build_config(self) 311 except BuildEnvironmentNotFoundException: 312 pass 313 314 return get_repository_object(self.topsrcdir) 315 316 def mozbuild_reader(self, config_mode='build', vcs_revision=None, 317 vcs_check_clean=True): 318 """Obtain a ``BuildReader`` for evaluating moz.build files. 319 320 Given arguments, returns a ``mozbuild.frontend.reader.BuildReader`` 321 that can be used to evaluate moz.build files for this repo. 322 323 ``config_mode`` is either ``build`` or ``empty``. If ``build``, 324 ``self.config_environment`` is used. This requires a configured build 325 system to work. If ``empty``, an empty config is used. ``empty`` is 326 appropriate for file-based traversal mode where ``Files`` metadata is 327 read. 328 329 If ``vcs_revision`` is defined, it specifies a version control revision 330 to use to obtain files content. The default is to use the filesystem. 331 This mode is only supported with Mercurial repositories. 332 333 If ``vcs_revision`` is not defined and the version control checkout is 334 sparse, this implies ``vcs_revision='.'``. 335 336 If ``vcs_revision`` is ``.`` (denotes the parent of the working 337 directory), we will verify that the working directory is clean unless 338 ``vcs_check_clean`` is False. This prevents confusion due to uncommitted 339 file changes not being reflected in the reader. 340 """ 341 from mozbuild.frontend.reader import ( 342 default_finder, 343 BuildReader, 344 EmptyConfig, 345 ) 346 from mozpack.files import ( 347 MercurialRevisionFinder, 348 ) 349 350 if config_mode == 'build': 351 config = self.config_environment 352 elif config_mode == 'empty': 353 config = EmptyConfig(self.topsrcdir) 354 else: 355 raise ValueError('unknown config_mode value: %s' % config_mode) 356 357 try: 358 repo = self.repository 359 except InvalidRepoPath: 360 repo = None 361 362 if repo and not vcs_revision and repo.sparse_checkout_present(): 363 vcs_revision = '.' 364 365 if vcs_revision is None: 366 finder = default_finder 367 else: 368 # If we failed to detect the repo prior, check again to raise its 369 # exception. 370 if not repo: 371 self.repository 372 assert False 373 374 if repo.name != 'hg': 375 raise Exception('do not support VCS reading mode for %s' % 376 repo.name) 377 378 if vcs_revision == '.' and vcs_check_clean: 379 with repo: 380 if not repo.working_directory_clean(): 381 raise Exception('working directory is not clean; ' 382 'refusing to use a VCS-based finder') 383 384 finder = MercurialRevisionFinder(self.topsrcdir, rev=vcs_revision, 385 recognize_repo_paths=True) 386 387 return BuildReader(config, finder=finder) 388 389 390 @memoized_property 391 def python3(self): 392 """Obtain info about a Python 3 executable. 393 394 Returns a tuple of an executable path and its version (as a tuple). 395 Either both entries will have a value or both will be None. 396 """ 397 # Search configured build info first. Then fall back to system. 398 try: 399 subst = self.substs 400 401 if 'PYTHON3' in subst: 402 version = tuple(map(int, subst['PYTHON3_VERSION'].split('.'))) 403 return subst['PYTHON3'], version 404 except BuildEnvironmentNotFoundException: 405 pass 406 407 return find_python3_executable() 408 409 def is_clobber_needed(self): 410 if not os.path.exists(self.topobjdir): 411 return False 412 return Clobberer(self.topsrcdir, self.topobjdir).clobber_needed() 413 414 def get_binary_path(self, what='app', validate_exists=True, where='default'): 415 """Obtain the path to a compiled binary for this build configuration. 416 417 The what argument is the program or tool being sought after. See the 418 code implementation for supported values. 419 420 If validate_exists is True (the default), we will ensure the found path 421 exists before returning, raising an exception if it doesn't. 422 423 If where is 'staged-package', we will return the path to the binary in 424 the package staging directory. 425 426 If no arguments are specified, we will return the main binary for the 427 configured XUL application. 428 """ 429 430 if where not in ('default', 'staged-package'): 431 raise Exception("Don't know location %s" % where) 432 433 substs = self.substs 434 435 stem = self.distdir 436 if where == 'staged-package': 437 stem = os.path.join(stem, substs['MOZ_APP_NAME']) 438 439 if substs['OS_ARCH'] == 'Darwin': 440 if substs['MOZ_BUILD_APP'] == 'xulrunner': 441 stem = os.path.join(stem, 'XUL.framework'); 442 else: 443 stem = os.path.join(stem, substs['MOZ_MACBUNDLE_NAME'], 'Contents', 444 'MacOS') 445 elif where == 'default': 446 stem = os.path.join(stem, 'bin') 447 448 leaf = None 449 450 leaf = (substs['MOZ_APP_NAME'] if what == 'app' else what) + substs['BIN_SUFFIX'] 451 path = os.path.join(stem, leaf) 452 453 if validate_exists and not os.path.exists(path): 454 raise Exception('Binary expected at %s does not exist.' % path) 455 456 return path 457 458 def resolve_config_guess(self): 459 make_extra = self.mozconfig['make_extra'] or [] 460 make_extra = dict(m.split('=', 1) for m in make_extra) 461 462 config_guess = make_extra.get('CONFIG_GUESS', None) 463 464 if config_guess: 465 return config_guess 466 467 # config.guess results should be constant for process lifetime. Cache 468 # it. 469 if _config_guess_output: 470 return _config_guess_output[0] 471 472 p = os.path.join(self.topsrcdir, 'build', 'autoconf', 'config.guess') 473 474 # This is a little kludgy. We need access to the normalize_command 475 # function. However, that's a method of a mach mixin, so we need a 476 # class instance. Ideally the function should be accessible as a 477 # standalone function. 478 o = MozbuildObject(self.topsrcdir, None, None, None) 479 args = o._normalize_command([p], True) 480 481 _config_guess_output.append( 482 subprocess.check_output(args, cwd=self.topsrcdir, shell=True).strip()) 483 return _config_guess_output[0] 484 485 def notify(self, msg): 486 """Show a desktop notification with the supplied message 487 488 On Linux and Mac, this will show a desktop notification with the message, 489 but on Windows we can only flash the screen. 490 """ 491 moz_nospam = os.environ.get('MOZ_NOSPAM') 492 if moz_nospam: 493 return 494 495 try: 496 if sys.platform.startswith('darwin'): 497 try: 498 notifier = which.which('terminal-notifier') 499 except which.WhichError: 500 raise Exception('Install terminal-notifier to get ' 501 'a notification when the build finishes.') 502 self.run_process([notifier, '-title', 503 'Mozilla Build System', '-group', 'mozbuild', 504 '-message', msg], ensure_exit_code=False) 505 elif sys.platform.startswith('linux'): 506 try: 507 notifier = which.which('notify-send') 508 except which.WhichError: 509 raise Exception('Install notify-send (usually part of ' 510 'the libnotify package) to get a notification when ' 511 'the build finishes.') 512 self.run_process([notifier, '--app-name=Mozilla Build System', 513 'Mozilla Build System', msg], ensure_exit_code=False) 514 elif sys.platform.startswith('win'): 515 from ctypes import Structure, windll, POINTER, sizeof 516 from ctypes.wintypes import DWORD, HANDLE, WINFUNCTYPE, BOOL, UINT 517 class FLASHWINDOW(Structure): 518 _fields_ = [("cbSize", UINT), 519 ("hwnd", HANDLE), 520 ("dwFlags", DWORD), 521 ("uCount", UINT), 522 ("dwTimeout", DWORD)] 523 FlashWindowExProto = WINFUNCTYPE(BOOL, POINTER(FLASHWINDOW)) 524 FlashWindowEx = FlashWindowExProto(("FlashWindowEx", windll.user32)) 525 FLASHW_CAPTION = 0x01 526 FLASHW_TRAY = 0x02 527 FLASHW_TIMERNOFG = 0x0C 528 529 # GetConsoleWindows returns NULL if no console is attached. We 530 # can't flash nothing. 531 console = windll.kernel32.GetConsoleWindow() 532 if not console: 533 return 534 535 params = FLASHWINDOW(sizeof(FLASHWINDOW), 536 console, 537 FLASHW_CAPTION | FLASHW_TRAY | FLASHW_TIMERNOFG, 3, 0) 538 FlashWindowEx(params) 539 except Exception as e: 540 self.log(logging.WARNING, 'notifier-failed', {'error': 541 e.message}, 'Notification center failed: {error}') 542 543 def _ensure_objdir_exists(self): 544 if os.path.isdir(self.statedir): 545 return 546 547 os.makedirs(self.statedir) 548 549 def _ensure_state_subdir_exists(self, subdir): 550 path = os.path.join(self.statedir, subdir) 551 552 if os.path.isdir(path): 553 return 554 555 os.makedirs(path) 556 557 def _get_state_filename(self, filename, subdir=None): 558 path = self.statedir 559 560 if subdir: 561 path = os.path.join(path, subdir) 562 563 return os.path.join(path, filename) 564 565 def _wrap_path_argument(self, arg): 566 return PathArgument(arg, self.topsrcdir, self.topobjdir) 567 568 def _run_make(self, directory=None, filename=None, target=None, log=True, 569 srcdir=False, allow_parallel=True, line_handler=None, 570 append_env=None, explicit_env=None, ignore_errors=False, 571 ensure_exit_code=0, silent=True, print_directory=True, 572 pass_thru=False, num_jobs=0, keep_going=False): 573 """Invoke make. 574 575 directory -- Relative directory to look for Makefile in. 576 filename -- Explicit makefile to run. 577 target -- Makefile target(s) to make. Can be a string or iterable of 578 strings. 579 srcdir -- If True, invoke make from the source directory tree. 580 Otherwise, make will be invoked from the object directory. 581 silent -- If True (the default), run make in silent mode. 582 print_directory -- If True (the default), have make print directories 583 while doing traversal. 584 """ 585 self._ensure_objdir_exists() 586 587 args = self._make_path() 588 589 if directory: 590 args.extend(['-C', directory.replace(os.sep, '/')]) 591 592 if filename: 593 args.extend(['-f', filename]) 594 595 if num_jobs == 0 and self.mozconfig['make_flags']: 596 flags = iter(self.mozconfig['make_flags']) 597 for flag in flags: 598 if flag == '-j': 599 try: 600 flag = flags.next() 601 except StopIteration: 602 break 603 try: 604 num_jobs = int(flag) 605 except ValueError: 606 args.append(flag) 607 elif flag.startswith('-j'): 608 try: 609 num_jobs = int(flag[2:]) 610 except (ValueError, IndexError): 611 break 612 else: 613 args.append(flag) 614 615 if allow_parallel: 616 if num_jobs > 0: 617 args.append('-j%d' % num_jobs) 618 else: 619 args.append('-j%d' % multiprocessing.cpu_count()) 620 elif num_jobs > 0: 621 args.append('MOZ_PARALLEL_BUILD=%d' % num_jobs) 622 623 if ignore_errors: 624 args.append('-k') 625 626 if silent: 627 args.append('-s') 628 629 # Print entering/leaving directory messages. Some consumers look at 630 # these to measure progress. 631 if print_directory: 632 args.append('-w') 633 634 if keep_going: 635 args.append('-k') 636 637 if isinstance(target, list): 638 args.extend(target) 639 elif target: 640 args.append(target) 641 642 fn = self._run_command_in_objdir 643 644 if srcdir: 645 fn = self._run_command_in_srcdir 646 647 append_env = dict(append_env or ()) 648 append_env[b'MACH'] = '1' 649 650 params = { 651 'args': args, 652 'line_handler': line_handler, 653 'append_env': append_env, 654 'explicit_env': explicit_env, 655 'log_level': logging.INFO, 656 'require_unix_environment': False, 657 'ensure_exit_code': ensure_exit_code, 658 'pass_thru': pass_thru, 659 660 # Make manages its children, so mozprocess doesn't need to bother. 661 # Having mozprocess manage children can also have side-effects when 662 # building on Windows. See bug 796840. 663 'ignore_children': True, 664 } 665 666 if log: 667 params['log_name'] = 'make' 668 669 return fn(**params) 670 671 def _make_path(self): 672 baseconfig = os.path.join(self.topsrcdir, 'config', 'baseconfig.mk') 673 674 def is_xcode_lisense_error(output): 675 return self._is_osx() and b'Agreeing to the Xcode' in output 676 677 def validate_make(make): 678 if os.path.exists(baseconfig) and os.path.exists(make): 679 cmd = [make, '-f', baseconfig] 680 if self._is_windows(): 681 cmd.append('HOST_OS_ARCH=WINNT') 682 try: 683 subprocess.check_output(cmd, stderr=subprocess.STDOUT) 684 except subprocess.CalledProcessError as e: 685 return False, is_xcode_lisense_error(e.output) 686 return True, False 687 return False, False 688 689 xcode_lisense_error = False 690 possible_makes = ['gmake', 'make', 'mozmake', 'gnumake', 'mingw32-make'] 691 692 if 'MAKE' in os.environ: 693 make = os.environ['MAKE'] 694 possible_makes.insert(0, make) 695 696 for test in possible_makes: 697 if os.path.isabs(test): 698 make = test 699 else: 700 try: 701 make = which.which(test) 702 except which.WhichError: 703 continue 704 result, xcode_lisense_error_tmp = validate_make(make) 705 if result: 706 return [make] 707 if xcode_lisense_error_tmp: 708 xcode_lisense_error = True 709 710 if xcode_lisense_error: 711 raise Exception('Xcode requires accepting to the license agreement.\n' 712 'Please run Xcode and accept the license agreement.') 713 714 if self._is_windows(): 715 raise Exception('Could not find a suitable make implementation.\n' 716 'Please use MozillaBuild 1.9 or newer') 717 else: 718 raise Exception('Could not find a suitable make implementation.') 719 720 def _run_command_in_srcdir(self, **args): 721 return self.run_process(cwd=self.topsrcdir, **args) 722 723 def _run_command_in_objdir(self, **args): 724 return self.run_process(cwd=self.topobjdir, **args) 725 726 def _is_windows(self): 727 return os.name in ('nt', 'ce') 728 729 def _is_osx(self): 730 return 'darwin' in str(sys.platform).lower() 731 732 def _spawn(self, cls): 733 """Create a new MozbuildObject-derived class instance from ourselves. 734 735 This is used as a convenience method to create other 736 MozbuildObject-derived class instances. It can only be used on 737 classes that have the same constructor arguments as us. 738 """ 739 740 return cls(self.topsrcdir, self.settings, self.log_manager, 741 topobjdir=self.topobjdir) 742 743 def _activate_virtualenv(self): 744 self.virtualenv_manager.ensure() 745 self.virtualenv_manager.activate() 746 747 748 def _set_log_level(self, verbose): 749 self.log_manager.terminal_handler.setLevel(logging.INFO if not verbose else logging.DEBUG) 750 751 752class MachCommandBase(MozbuildObject): 753 """Base class for mach command providers that wish to be MozbuildObjects. 754 755 This provides a level of indirection so MozbuildObject can be refactored 756 without having to change everything that inherits from it. 757 """ 758 759 def __init__(self, context): 760 # Attempt to discover topobjdir through environment detection, as it is 761 # more reliable than mozconfig when cwd is inside an objdir. 762 topsrcdir = context.topdir 763 topobjdir = None 764 detect_virtualenv_mozinfo = True 765 if hasattr(context, 'detect_virtualenv_mozinfo'): 766 detect_virtualenv_mozinfo = getattr(context, 767 'detect_virtualenv_mozinfo') 768 try: 769 dummy = MozbuildObject.from_environment(cwd=context.cwd, 770 detect_virtualenv_mozinfo=detect_virtualenv_mozinfo) 771 topsrcdir = dummy.topsrcdir 772 topobjdir = dummy._topobjdir 773 if topobjdir: 774 # If we're inside a objdir and the found mozconfig resolves to 775 # another objdir, we abort. The reasoning here is that if you 776 # are inside an objdir you probably want to perform actions on 777 # that objdir, not another one. This prevents accidental usage 778 # of the wrong objdir when the current objdir is ambiguous. 779 config_topobjdir = dummy.resolve_mozconfig_topobjdir() 780 781 if config_topobjdir and not samepath(topobjdir, config_topobjdir): 782 raise ObjdirMismatchException(topobjdir, config_topobjdir) 783 except BuildEnvironmentNotFoundException: 784 pass 785 except ObjdirMismatchException as e: 786 print('Ambiguous object directory detected. We detected that ' 787 'both %s and %s could be object directories. This is ' 788 'typically caused by having a mozconfig pointing to a ' 789 'different object directory from the current working ' 790 'directory. To solve this problem, ensure you do not have a ' 791 'default mozconfig in searched paths.' % (e.objdir1, 792 e.objdir2)) 793 sys.exit(1) 794 795 except MozconfigLoadException as e: 796 print('Error loading mozconfig: ' + e.path) 797 print('') 798 print(e.message) 799 if e.output: 800 print('') 801 print('mozconfig output:') 802 print('') 803 for line in e.output: 804 print(line) 805 806 sys.exit(1) 807 808 MozbuildObject.__init__(self, topsrcdir, context.settings, 809 context.log_manager, topobjdir=topobjdir) 810 811 self._mach_context = context 812 813 # Incur mozconfig processing so we have unified error handling for 814 # errors. Otherwise, the exceptions could bubble back to mach's error 815 # handler. 816 try: 817 self.mozconfig 818 819 except MozconfigFindException as e: 820 print(e.message) 821 sys.exit(1) 822 823 except MozconfigLoadException as e: 824 print('Error loading mozconfig: ' + e.path) 825 print('') 826 print(e.message) 827 if e.output: 828 print('') 829 print('mozconfig output:') 830 print('') 831 for line in e.output: 832 print(line) 833 834 sys.exit(1) 835 836 # Always keep a log of the last command, but don't do that for mach 837 # invokations from scripts (especially not the ones done by the build 838 # system itself). 839 if (os.isatty(sys.stdout.fileno()) and 840 not getattr(self, 'NO_AUTO_LOG', False)): 841 self._ensure_state_subdir_exists('.') 842 logfile = self._get_state_filename('last_log.json') 843 try: 844 fd = open(logfile, "wb") 845 self.log_manager.add_json_handler(fd) 846 except Exception as e: 847 self.log(logging.WARNING, 'mach', {'error': e}, 848 'Log will not be kept for this command: {error}.') 849 850 851class MachCommandConditions(object): 852 """A series of commonly used condition functions which can be applied to 853 mach commands with providers deriving from MachCommandBase. 854 """ 855 @staticmethod 856 def is_firefox(cls): 857 """Must have a Firefox build.""" 858 if hasattr(cls, 'substs'): 859 return cls.substs.get('MOZ_BUILD_APP') == 'browser' 860 return False 861 862 @staticmethod 863 def is_android(cls): 864 """Must have an Android build.""" 865 if hasattr(cls, 'substs'): 866 return cls.substs.get('MOZ_WIDGET_TOOLKIT') == 'android' 867 return False 868 869 @staticmethod 870 def is_hg(cls): 871 """Must have a mercurial source checkout.""" 872 return getattr(cls, 'substs', {}).get('VCS_CHECKOUT_TYPE') == 'hg' 873 874 @staticmethod 875 def is_git(cls): 876 """Must have a git source checkout.""" 877 return getattr(cls, 'substs', {}).get('VCS_CHECKOUT_TYPE') == 'git' 878 879 @staticmethod 880 def is_artifact_build(cls): 881 """Must be an artifact build.""" 882 return getattr(cls, 'substs', {}).get('MOZ_ARTIFACT_BUILDS') 883 884 885class PathArgument(object): 886 """Parse a filesystem path argument and transform it in various ways.""" 887 888 def __init__(self, arg, topsrcdir, topobjdir, cwd=None): 889 self.arg = arg 890 self.topsrcdir = topsrcdir 891 self.topobjdir = topobjdir 892 self.cwd = os.getcwd() if cwd is None else cwd 893 894 def relpath(self): 895 """Return a path relative to the topsrcdir or topobjdir. 896 897 If the argument is a path to a location in one of the base directories 898 (topsrcdir or topobjdir), then strip off the base directory part and 899 just return the path within the base directory.""" 900 901 abspath = os.path.abspath(os.path.join(self.cwd, self.arg)) 902 903 # If that path is within topsrcdir or topobjdir, return an equivalent 904 # path relative to that base directory. 905 for base_dir in [self.topobjdir, self.topsrcdir]: 906 if abspath.startswith(os.path.abspath(base_dir)): 907 return mozpath.relpath(abspath, base_dir) 908 909 return mozpath.normsep(self.arg) 910 911 def srcdir_path(self): 912 return mozpath.join(self.topsrcdir, self.relpath()) 913 914 def objdir_path(self): 915 return mozpath.join(self.topobjdir, self.relpath()) 916 917 918class ExecutionSummary(dict): 919 """Helper for execution summaries.""" 920 921 def __init__(self, summary_format, **data): 922 self._summary_format = '' 923 assert 'execution_time' in data 924 self.extend(summary_format, **data) 925 926 def extend(self, summary_format, **data): 927 self._summary_format += summary_format 928 self.update(data) 929 930 def __str__(self): 931 return self._summary_format.format(**self) 932 933 def __getattr__(self, key): 934 return self[key] 935