| # This Source Code Form is subject to the terms of the Mozilla Public |
| # License, v. 2.0. If a copy of the MPL was not distributed with this |
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| |
| from __future__ import print_function, unicode_literals |
| |
| import errno |
| import os |
| import platform |
| import sys |
| import time |
| import __builtin__ |
| |
| from types import ModuleType |
| |
| |
| STATE_DIR_FIRST_RUN = ''' |
| mach and the build system store shared state in a common directory on the |
| filesystem. The following directory will be created: |
| |
| {userdir} |
| |
| If you would like to use a different directory, hit CTRL+c and set the |
| MOZBUILD_STATE_PATH environment variable to the directory you would like to |
| use and re-run mach. For this change to take effect forever, you'll likely |
| want to export this environment variable from your shell's init scripts. |
| '''.lstrip() |
| |
| NO_MERCURIAL_SETUP = ''' |
| *** MERCURIAL NOT CONFIGURED *** |
| |
| mach has detected that you have never run `{mach} mercurial-setup`. |
| |
| Running this command will ensure your Mercurial version control tool is up |
| to date and optimally configured for a better, more productive experience |
| when working on Mozilla projects. |
| |
| Please run `{mach} mercurial-setup` now. |
| |
| Note: `{mach} mercurial-setup` does not make any changes without prompting |
| you first. |
| '''.strip() |
| |
| MERCURIAL_SETUP_FATAL_INTERVAL = 31 * 24 * 60 * 60 |
| |
| |
| # TODO Bug 794506 Integrate with the in-tree virtualenv configuration. |
| SEARCH_PATHS = [ |
| 'python/mach', |
| 'python/mozboot', |
| 'python/mozbuild', |
| 'python/mozversioncontrol', |
| 'python/blessings', |
| 'python/compare-locales', |
| 'python/configobj', |
| 'python/jsmin', |
| 'python/psutil', |
| 'python/which', |
| 'python/pystache', |
| 'python/pyyaml/lib', |
| 'python/requests', |
| 'python/slugid', |
| 'build', |
| 'config', |
| 'dom/bindings', |
| 'dom/bindings/parser', |
| 'layout/tools/reftest', |
| 'other-licenses/ply', |
| 'testing', |
| 'testing/firefox-ui/harness', |
| 'testing/firefox-ui/tests', |
| 'testing/luciddream', |
| 'testing/marionette/client', |
| 'testing/marionette/client/marionette/runner/mixins/browsermob-proxy-py', |
| 'testing/marionette/transport', |
| 'testing/marionette/driver', |
| 'testing/mozbase/mozcrash', |
| 'testing/mozbase/mozdebug', |
| 'testing/mozbase/mozdevice', |
| 'testing/mozbase/mozfile', |
| 'testing/mozbase/mozhttpd', |
| 'testing/mozbase/mozinfo', |
| 'testing/mozbase/mozinstall', |
| 'testing/mozbase/mozleak', |
| 'testing/mozbase/mozlog', |
| 'testing/mozbase/moznetwork', |
| 'testing/mozbase/mozprocess', |
| 'testing/mozbase/mozprofile', |
| 'testing/mozbase/mozrunner', |
| 'testing/mozbase/mozsystemmonitor', |
| 'testing/mozbase/mozscreenshot', |
| 'testing/mozbase/moztest', |
| 'testing/mozbase/mozversion', |
| 'testing/mozbase/manifestparser', |
| 'testing/puppeteer/firefox', |
| 'testing/taskcluster', |
| 'testing/tools/autotry', |
| 'testing/web-platform', |
| 'testing/web-platform/harness', |
| 'testing/web-platform/tests/tools/wptserve', |
| 'testing/xpcshell', |
| 'xpcom/idl-parser', |
| ] |
| |
| # Individual files providing mach commands. |
| MACH_MODULES = [ |
| 'addon-sdk/mach_commands.py', |
| 'build/valgrind/mach_commands.py', |
| 'dom/bindings/mach_commands.py', |
| 'layout/tools/reftest/mach_commands.py', |
| 'python/mach_commands.py', |
| 'python/mach/mach/commands/commandinfo.py', |
| 'python/compare-locales/mach_commands.py', |
| 'python/mozboot/mozboot/mach_commands.py', |
| 'python/mozbuild/mozbuild/mach_commands.py', |
| 'python/mozbuild/mozbuild/backend/mach_commands.py', |
| 'python/mozbuild/mozbuild/compilation/codecomplete.py', |
| 'python/mozbuild/mozbuild/frontend/mach_commands.py', |
| 'services/common/tests/mach_commands.py', |
| 'testing/firefox-ui/mach_commands.py', |
| 'testing/luciddream/mach_commands.py', |
| 'testing/mach_commands.py', |
| 'testing/marionette/mach_commands.py', |
| 'testing/mochitest/mach_commands.py', |
| 'testing/mozharness/mach_commands.py', |
| 'testing/talos/mach_commands.py', |
| 'testing/taskcluster/mach_commands.py', |
| 'testing/web-platform/mach_commands.py', |
| 'testing/xpcshell/mach_commands.py', |
| 'tools/docs/mach_commands.py', |
| 'tools/mercurial/mach_commands.py', |
| 'tools/mach_commands.py', |
| 'tools/power/mach_commands.py', |
| 'mobile/android/mach_commands.py', |
| ] |
| |
| |
| CATEGORIES = { |
| 'build': { |
| 'short': 'Build Commands', |
| 'long': 'Interact with the build system', |
| 'priority': 80, |
| }, |
| 'post-build': { |
| 'short': 'Post-build Commands', |
| 'long': 'Common actions performed after completing a build.', |
| 'priority': 70, |
| }, |
| 'testing': { |
| 'short': 'Testing', |
| 'long': 'Run tests.', |
| 'priority': 60, |
| }, |
| 'ci': { |
| 'short': 'CI', |
| 'long': 'Taskcluster commands', |
| 'priority': 59 |
| }, |
| 'devenv': { |
| 'short': 'Development Environment', |
| 'long': 'Set up and configure your development environment.', |
| 'priority': 50, |
| }, |
| 'build-dev': { |
| 'short': 'Low-level Build System Interaction', |
| 'long': 'Interact with specific parts of the build system.', |
| 'priority': 20, |
| }, |
| 'misc': { |
| 'short': 'Potpourri', |
| 'long': 'Potent potables and assorted snacks.', |
| 'priority': 10, |
| }, |
| 'disabled': { |
| 'short': 'Disabled', |
| 'long': 'The disabled commands are hidden by default. Use -v to display them. These commands are unavailable for your current context, run "mach <command>" to see why.', |
| 'priority': 0, |
| } |
| } |
| |
| |
| def get_state_dir(): |
| """Obtain the path to a directory to hold state. |
| |
| Returns a tuple of the path and a bool indicating whether the value came |
| from an environment variable. |
| """ |
| state_user_dir = os.path.expanduser('~/.mozbuild') |
| state_env_dir = os.environ.get('MOZBUILD_STATE_PATH', None) |
| |
| if state_env_dir: |
| return state_env_dir, True |
| else: |
| return state_user_dir, False |
| |
| |
| def bootstrap(topsrcdir, mozilla_dir=None): |
| if mozilla_dir is None: |
| mozilla_dir = topsrcdir |
| |
| # Ensure we are running Python 2.7+. We put this check here so we generate a |
| # user-friendly error message rather than a cryptic stack trace on module |
| # import. |
| if sys.version_info[0] != 2 or sys.version_info[1] < 7: |
| print('Python 2.7 or above (but not Python 3) is required to run mach.') |
| print('You are running Python', platform.python_version()) |
| sys.exit(1) |
| |
| # Global build system and mach state is stored in a central directory. By |
| # default, this is ~/.mozbuild. However, it can be defined via an |
| # environment variable. We detect first run (by lack of this directory |
| # existing) and notify the user that it will be created. The logic for |
| # creation is much simpler for the "advanced" environment variable use |
| # case. For default behavior, we educate users and give them an opportunity |
| # to react. We always exit after creating the directory because users don't |
| # like surprises. |
| try: |
| import mach.main |
| except ImportError: |
| sys.path[0:0] = [os.path.join(mozilla_dir, path) for path in SEARCH_PATHS] |
| import mach.main |
| |
| def pre_dispatch_handler(context, handler, args): |
| """Perform global checks before command dispatch. |
| |
| Currently, our goal is to ensure developers periodically run |
| `mach mercurial-setup` (when applicable) to ensure their Mercurial |
| tools are up to date. |
| """ |
| # Don't do anything when... |
| |
| # The user is performing a maintenance command. |
| if handler.name in ('bootstrap', 'doctor', 'mach-commands', 'mercurial-setup'): |
| return |
| |
| # We are running in automation. |
| if 'MOZ_AUTOMATION' in os.environ or 'TASK_ID' in os.environ: |
| return |
| |
| # We are a curmudgeon who has found this undocumented variable. |
| if 'I_PREFER_A_SUBOPTIMAL_MERCURIAL_EXPERIENCE' in os.environ: |
| return |
| |
| # The environment is likely a machine invocation. |
| if sys.stdin.closed or not sys.stdin.isatty(): |
| return |
| |
| # Mercurial isn't managing this source checkout. |
| if not os.path.exists(os.path.join(topsrcdir, '.hg')): |
| return |
| |
| state_dir = get_state_dir()[0] |
| last_check_path = os.path.join(state_dir, 'mercurial', |
| 'setup.lastcheck') |
| |
| mtime = None |
| try: |
| mtime = os.path.getmtime(last_check_path) |
| except OSError as e: |
| if e.errno != errno.ENOENT: |
| raise |
| |
| # No last run file means mercurial-setup has never completed. |
| if mtime is None: |
| print(NO_MERCURIAL_SETUP.format(mach=sys.argv[0]), file=sys.stderr) |
| sys.exit(2) |
| |
| def populate_context(context, key=None): |
| if key is None: |
| return |
| if key == 'state_dir': |
| state_dir, is_environ = get_state_dir() |
| if is_environ: |
| if not os.path.exists(state_dir): |
| print('Creating global state directory from environment variable: %s' |
| % state_dir) |
| os.makedirs(state_dir, mode=0o770) |
| print('Please re-run mach.') |
| sys.exit(1) |
| else: |
| if not os.path.exists(state_dir): |
| print(STATE_DIR_FIRST_RUN.format(userdir=state_dir)) |
| try: |
| for i in range(20, -1, -1): |
| time.sleep(1) |
| sys.stdout.write('%d ' % i) |
| sys.stdout.flush() |
| except KeyboardInterrupt: |
| sys.exit(1) |
| |
| print('\nCreating default state directory: %s' % state_dir) |
| os.mkdir(state_dir) |
| print('Please re-run mach.') |
| sys.exit(1) |
| |
| return state_dir |
| |
| if key == 'topdir': |
| return topsrcdir |
| |
| if key == 'pre_dispatch_handler': |
| return pre_dispatch_handler |
| |
| raise AttributeError(key) |
| |
| mach = mach.main.Mach(os.getcwd()) |
| mach.populate_context_handler = populate_context |
| |
| for category, meta in CATEGORIES.items(): |
| mach.define_category(category, meta['short'], meta['long'], |
| meta['priority']) |
| |
| for path in MACH_MODULES: |
| mach.load_commands_from_file(os.path.join(mozilla_dir, path)) |
| |
| return mach |
| |
| |
| # Hook import such that .pyc/.pyo files without a corresponding .py file in |
| # the source directory are essentially ignored. See further below for details |
| # and caveats. |
| # Objdirs outside the source directory are ignored because in most cases, if |
| # a .pyc/.pyo file exists there, a .py file will be next to it anyways. |
| class ImportHook(object): |
| def __init__(self, original_import): |
| self._original_import = original_import |
| # Assume the source directory is the parent directory of the one |
| # containing this file. |
| self._source_dir = os.path.normcase(os.path.abspath( |
| os.path.dirname(os.path.dirname(__file__)))) + os.sep |
| self._modules = set() |
| |
| def __call__(self, name, globals=None, locals=None, fromlist=None, |
| level=-1): |
| # name might be a relative import. Instead of figuring out what that |
| # resolves to, which is complex, just rely on the real import. |
| # Since we don't know the full module name, we can't check sys.modules, |
| # so we need to keep track of which modules we've already seen to avoid |
| # to stat() them again when they are imported multiple times. |
| module = self._original_import(name, globals, locals, fromlist, level) |
| |
| # Some tests replace modules in sys.modules with non-module instances. |
| if not isinstance(module, ModuleType): |
| return module |
| |
| resolved_name = module.__name__ |
| if resolved_name in self._modules: |
| return module |
| self._modules.add(resolved_name) |
| |
| # Builtin modules don't have a __file__ attribute. |
| if not hasattr(module, '__file__'): |
| return module |
| |
| # Note: module.__file__ is not always absolute. |
| path = os.path.normcase(os.path.abspath(module.__file__)) |
| # Note: we could avoid normcase and abspath above for non pyc/pyo |
| # files, but those are actually rare, so it doesn't really matter. |
| if not path.endswith(('.pyc', '.pyo')): |
| return module |
| |
| # Ignore modules outside our source directory |
| if not path.startswith(self._source_dir): |
| return module |
| |
| # If there is no .py corresponding to the .pyc/.pyo module we're |
| # loading, remove the .pyc/.pyo file, and reload the module. |
| # Since we already loaded the .pyc/.pyo module, if it had side |
| # effects, they will have happened already, and loading the module |
| # with the same name, from another directory may have the same side |
| # effects (or different ones). We assume it's not a problem for the |
| # python modules under our source directory (either because it |
| # doesn't happen or because it doesn't matter). |
| if not os.path.exists(module.__file__[:-1]): |
| os.remove(module.__file__) |
| del sys.modules[module.__name__] |
| module = self(name, globals, locals, fromlist, level) |
| |
| return module |
| |
| |
| # Install our hook |
| __builtin__.__import__ = ImportHook(__builtin__.__import__) |