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