Bug 799262 - Formal API for loading mach command modules; r=jhammel
--- a/mach
+++ b/mach
@@ -39,9 +39,10 @@ try:
import mach.main
except ImportError:
sys.path[0:0] = [os.path.join(our_dir, path) for path in SEARCH_PATHS]
import mach.main
# All of the code is in a module because EVERYTHING IS A LIBRARY.
mach = mach.main.Mach(our_dir)
+mach.load_commands_from_sys_path()
sys.exit(mach.run(sys.argv[1:]))
--- a/python/mach/mach/main.py
+++ b/python/mach/mach/main.py
@@ -8,16 +8,17 @@
from __future__ import unicode_literals
import argparse
import codecs
import imp
import logging
import os
import sys
+import uuid
from mozbuild.base import BuildConfig
from mozbuild.config import ConfigSettings
from mozbuild.logger import LoggingManager
from mach.registrar import populate_argument_parser
@@ -35,18 +36,16 @@ CONSUMED_ARGUMENTS = [
'logfile',
'log_interval',
'command',
'cls',
'method',
'func',
]
-MODULES_SCANNED = False
-
class ArgumentParser(argparse.ArgumentParser):
"""Custom implementation argument parser to make things look pretty."""
def error(self, message):
"""Custom error reporter to give more helpful text on bad commands."""
if not message.startswith('argument command: invalid choice'):
argparse.ArgumentParser.error(self, message)
@@ -108,20 +107,64 @@ To see more help for a specific command,
self.cwd = cwd
self.log_manager = LoggingManager()
self.logger = logging.getLogger(__name__)
self.settings = ConfigSettings()
self.log_manager.register_structured_logger(self.logger)
- if not MODULES_SCANNED:
- self._load_modules()
+ def load_commands_from_sys_path(self):
+ """Discover and load mach command modules from sys.path.
+
+ This iterates over all paths on sys.path. If the path contains a
+ "mach/commands" subdirectory, all .py files in that directory will be
+ loaded and examined for mach commands.
+ """
+ # Create parent module otherwise Python complains when the parent is
+ # missing.
+ if b'mach.commands' not in sys.modules:
+ mod = imp.new_module(b'mach.commands')
+ sys.modules[b'mach.commands'] = mod
+
+ for path in sys.path:
+ # We only support importing .py files from directories.
+ commands_path = os.path.join(path, 'mach', 'commands')
+
+ if not os.path.isdir(commands_path):
+ continue
+
+ self.load_commands_from_directory(commands_path)
- MODULES_SCANNED = True
+ def load_commands_from_directory(self, path):
+ """Scan for mach commands from modules in a directory.
+
+ This takes a path to a directory, loads the .py files in it, and
+ registers and found mach command providers with this mach instance.
+ """
+ for f in sorted(os.listdir(path)):
+ if not f.endswith('.py') or f == '__init__.py':
+ continue
+
+ full_path = os.path.join(path, f)
+ module_name = 'mach.commands.%s' % f[0:-3]
+
+ self.load_commands_from_file(full_path, module_name=module_name)
+
+ def load_commands_from_file(self, path, module_name=None):
+ """Scan for mach commands from a file.
+
+ This takes a path to a file and loads it as a Python module under the
+ module name specified. If no name is specified, a random one will be
+ chosen.
+ """
+ if module_name is None:
+ module_name = 'mach.commands.%s' % uuid.uuid1().get_hex()
+
+ imp.load_source(module_name, path)
def run(self, argv):
"""Runs mach with arguments provided from the command line.
Returns the integer exit code that should be used. 0 means success. All
other values indicate failure.
"""
@@ -212,45 +255,16 @@ To see more help for a specific command,
return result
def log(self, level, action, params, format_str):
"""Helper method to record a structured log event."""
self.logger.log(level, format_str,
extra={'action': action, 'params': params})
- def _load_modules(self):
- """Scan over Python modules looking for mach command providers."""
-
- # Create parent module otherwise Python complains when the parent is
- # missing.
- if b'mach.commands' not in sys.modules:
- mod = imp.new_module(b'mach.commands')
- sys.modules[b'mach.commands'] = mod
-
- for path in sys.path:
- # We only support importing .py files from directories.
- commands_path = os.path.join(path, 'mach', 'commands')
-
- if not os.path.isdir(commands_path):
- continue
-
- # We only support loading modules in the immediate mach.commands
- # module, not sub-modules. Walking the tree would be trivial to
- # implement if it were ever desired.
- for f in sorted(os.listdir(commands_path)):
- if not f.endswith('.py') or f == '__init__.py':
- continue
-
- full_path = os.path.join(commands_path, f)
- module_name = 'mach.commands.%s' % f[0:-3]
-
- imp.load_source(module_name, full_path)
-
-
def load_settings(self, args):
"""Determine which settings files apply and load them.
Currently, we only support loading settings from a single file.
Ideally, we support loading from multiple files. This is supported by
the ConfigSettings API. However, that API currently doesn't track where
individual values come from, so if we load from multiple sources then
save, we effectively do a full copy. We don't want this. Until