Bug 1255450 - [mach] Enable runtime configuration files, r?gps draft
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Mon, 28 Mar 2016 11:18:24 -0400
changeset 348080 0e484e09fd9dafd4b85857fc7a631dd0546545d6
parent 348079 cef41c963cef2d68b7e98088e9cec99e23ee6eb3
child 348081 0045c10d73b9e1007a2d83b8e63ddc0d421a9485
push id14745
push userahalberstadt@mozilla.com
push dateWed, 06 Apr 2016 16:15:26 +0000
Bug 1255450 - [mach] Enable runtime configuration files, r?gps Runtime configs have been implemented for awhile, but disabled. This patch enables configuration. Config files will be loaded in the following order (later files override earlier ones): 1a. $MACHRC 1b. $MOZBUILD_STATE_PATH/machrc (if $MACHRC is unset) 2. topsrcdir/machrc 3. CLI via --settings Note: .machrc may be used instead of machrc if desired. MozReview-Commit-ID: IntONAZLGML
--- a/.gitignore
+++ b/.gitignore
@@ -23,17 +23,18 @@ ID
 # User files that may appear at the root
 # Empty marker file that's generated when we check out NSS
 # Build directories
 # Build directories for js shell
--- a/.hgignore
+++ b/.hgignore
@@ -19,17 +19,17 @@
 # User files that may appear at the root
 # Empty marker file that's generated when we check out NSS
 # Build directories
 # Build directories for js shell
--- a/build/mach_bootstrap.py
+++ b/build/mach_bootstrap.py
@@ -115,16 +115,17 @@ SEARCH_PATHS = [
+    'python/mach/mach/commands/settings.py',
@@ -395,16 +396,22 @@ def bootstrap(topsrcdir, mozilla_dir=Non
         if key == 'post_dispatch_handler':
             return post_dispatch_handler
         raise AttributeError(key)
     mach = mach.main.Mach(os.getcwd())
     mach.populate_context_handler = populate_context
+    if not mach.settings_paths:
+        # default global machrc location
+        mach.settings_paths.append(get_state_dir()[0])
+    # always load local repository configuration
+    mach.settings_paths.append(mozilla_dir)
     for category, meta in CATEGORIES.items():
         mach.define_category(category, meta['short'], meta['long'],
     for path in MACH_MODULES:
         mach.load_commands_from_file(os.path.join(mozilla_dir, path))
     return mach
--- a/python/mach/docs/index.rst
+++ b/python/mach/docs/index.rst
@@ -67,8 +67,9 @@ mach is suitable for anybody to use. Unt
 best fit for you.
 .. toctree::
    :maxdepth: 1
+   settings
--- a/python/mach/mach/main.py
+++ b/python/mach/mach/main.py
@@ -180,16 +180,19 @@ class Mach(object):
             for use in command handlers.
             For backwards compatibility, it is also called before command
             dispatch without a key, allowing the context handler to add
             attributes to the context instance.
         require_conditions -- If True, commands that do not have any condition
             functions applied will be skipped. Defaults to False.
+        settings_paths -- A list of files or directories in which to search
+            for settings files to load.
     USAGE = """%(prog)s [global arguments] command [command arguments]
 mach (German for "do") is the main interface to the Mozilla build system and
 common developer tasks.
 You tell mach the command you want to perform and it does it for you.
@@ -206,16 +209,20 @@ To see more help for a specific command,
     def __init__(self, cwd):
         assert os.path.isdir(cwd)
         self.cwd = cwd
         self.log_manager = LoggingManager()
         self.logger = logging.getLogger(__name__)
         self.settings = ConfigSettings()
+        self.settings_paths = []
+        if 'MACHRC' in os.environ:
+            self.settings_paths.append(os.environ['MACHRC'])
         self.global_arguments = []
         self.populate_context_handler = None
     def add_global_argument(self, *args, **kwargs):
         """Register a global argument with the argument parser.
@@ -355,16 +362,22 @@ To see more help for a specific command,
             return 1
             sys.stdin = orig_stdin
             sys.stdout = orig_stdout
             sys.stderr = orig_stderr
     def _run(self, argv):
+        # Load settings as early as possible so things in dispatcher.py
+        # can use them.
+        for provider in Registrar.settings_providers:
+            self.settings.register_provider(provider)
+        self.load_settings(self.settings_paths)
         context = CommandContext(cwd=self.cwd,
             settings=self.settings, log_manager=self.log_manager,
         if self.populate_context_handler:
             context = ContextWrapper(context, self.populate_context_handler)
@@ -407,17 +420,20 @@ To see more help for a specific command,
         if args.log_no_times or 'MACH_NO_WRITE_TIMES' in os.environ:
             write_times = False
         # Always enable terminal logging. The log manager figures out if we are
         # actually in a TTY or are a pipe and does the right thing.
             write_interval=args.log_interval, write_times=write_times)
-        self.load_settings(args)
+        if args.settings_file:
+            # Argument parsing has already happened, so settings that apply
+            # to command line handling (e.g alias, defaults) will be ignored.
+            self.load_settings(args.settings_file)
         if not hasattr(args, 'mach_handler'):
             raise MachError('ArgumentParser result missing mach handler info.')
         handler = getattr(args, 'mach_handler')
             return Registrar._run_command_handler(handler, context=context,
@@ -487,67 +503,52 @@ To see more help for a specific command,
         for l in traceback.format_exception_only(exc_type, exc_value):
         for l in traceback.format_list(stack):
-    def load_settings(self, args):
-        """Determine which settings files apply and load them.
+    def load_settings(self, paths):
+        """Load the specified settings files.
-        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
-        ConfigSettings does the right thing, we shouldn't expose multi-file
-        loading.
+        If a directory is specified, the following basenames will be
+        searched for in this order:
-        We look for a settings file in the following locations. The first one
-        found wins:
-          1) Command line argument
-          2) Environment variable
-          3) Default path
+            machrc, .machrc
-        # Settings are disabled until integration with command providers is
-        # worked out.
-        self.settings = None
-        return False
+        if isinstance(paths, basestring):
+            paths = [paths]
-        for provider in Registrar.settings_providers:
-            provider.register_settings()
-            self.settings.register_provider(provider)
+        valid_names = ('machrc', '.machrc')
+        def find_in_dir(base):
+            if os.path.isfile(base):
+                return base
-        p = os.path.join(self.cwd, 'mach.ini')
+            for name in valid_names:
+                path = os.path.join(base, name)
+                if os.path.isfile(path):
+                    return path
-        if args.settings_file:
-            p = args.settings_file
-        elif 'MACH_SETTINGS_FILE' in os.environ:
-            p = os.environ['MACH_SETTINGS_FILE']
+        files = map(find_in_dir, self.settings_paths)
+        files = filter(bool, files)
-        self.settings.load_file(p)
-        return os.path.exists(p)
+        self.settings.load_files(files)
     def get_argument_parser(self, context):
         """Returns an argument parser for the command-line interface."""
         parser = ArgumentParser(add_help=False,
             usage='%(prog)s [global arguments] command [command arguments]')
         # Order is important here as it dictates the order the auto-generated
         # help messages are printed.
         global_group = parser.add_argument_group('Global Arguments')
-        #global_group.add_argument('--settings', dest='settings_file',
-        #    metavar='FILENAME', help='Path to settings file.')
         global_group.add_argument('-v', '--verbose', dest='verbose',
             action='store_true', default=False,
             help='Print verbose output.')
         global_group.add_argument('-l', '--log-file', dest='logfile',
             metavar='FILENAME', type=argparse.FileType('ab'),
             help='Filename to write log data to.')
         global_group.add_argument('--log-interval', dest='log_interval',
             action='store_true', default=False,
@@ -561,16 +562,19 @@ To see more help for a specific command,
             action='store_true', default=suppress_log_by_default,
             help='Do not prefix log lines with times. By default, mach will '
                 'prefix each output line with the time since command start.')
         global_group.add_argument('-h', '--help', dest='help',
             action='store_true', default=False,
             help='Show this help message.')
         global_group.add_argument('--debug-command', action='store_true',
             help='Start a Python debugger when command is dispatched.')
+        global_group.add_argument('--settings', dest='settings_file',
+            metavar='FILENAME', default=None,
+            help='Path to settings file.')
         for args, kwargs in self.global_arguments:
             global_group.add_argument(*args, **kwargs)
         # We need to be last because CommandAction swallows all remaining
         # arguments and argparse parses arguments in the order they were added.
         parser.add_argument('command', action=CommandAction,
             registrar=Registrar, context=context)