Bug 1255450 - [mach] Enable runtime configuration files, r=gps
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Mon, 28 Mar 2016 11:18:24 -0400
changeset 292818 84ba3a5bc33cc4af94f5a46465a6f921d794041b
parent 292817 b552927e1a1fe9f916811236eb30352be72eaaaf
child 292819 355e8bb48aeeef736df7ca21351f7e87caf90668
push id30167
push userkwierso@gmail.com
push dateTue, 12 Apr 2016 22:28:26 +0000
treeherdermozilla-central@fb125ff927ea [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs1255450
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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
.gitignore
.hgignore
build/mach_bootstrap.py
python/mach/docs/index.rst
python/mach/mach/main.py
--- a/.gitignore
+++ b/.gitignore
@@ -23,17 +23,18 @@ ID
 # User files that may appear at the root
 /.mozconfig*
 /mozconfig
 /configure
 /old-configure
 /config.cache
 /config.log
 /.clang_complete
-/mach.ini
+/machrc
+/.machrc
 
 # Empty marker file that's generated when we check out NSS
 security/manager/.nss.checkout
 
 # Build directories
 /obj*/
 
 # Build directories for js shell
--- a/.hgignore
+++ b/.hgignore
@@ -19,17 +19,17 @@
 # User files that may appear at the root
 ^\.mozconfig
 ^mozconfig*
 ^configure$
 ^old-configure$
 ^config\.cache$
 ^config\.log$
 ^\.clang_complete
-^mach.ini$
+^\.?machrc$
 
 # Empty marker file that's generated when we check out NSS
 ^security/manager/\.nss\.checkout$
 
 # Build directories
 ^obj
 
 # Build directories for js shell
--- a/build/mach_bootstrap.py
+++ b/build/mach_bootstrap.py
@@ -115,16 +115,17 @@ SEARCH_PATHS = [
 MACH_MODULES = [
     'addon-sdk/mach_commands.py',
     'build/valgrind/mach_commands.py',
     'dom/bindings/mach_commands.py',
     'dom/media/test/external/mach_commands.py',
     'layout/tools/reftest/mach_commands.py',
     'python/mach_commands.py',
     'python/mach/mach/commands/commandinfo.py',
+    'python/mach/mach/commands/settings.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',
@@ -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'],
             meta['priority'])
 
     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
 
    commands
    driver
    logging
+   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.log_manager.register_structured_logger(self.logger)
         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
 
         finally:
             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,
             commands=Registrar)
 
         if self.populate_context_handler:
             self.populate_context_handler(context)
             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.
         self.log_manager.add_terminal_logging(level=log_level,
             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')
 
         try:
             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):
             fh.write(l)
 
         fh.write('\n')
         for l in traceback.format_list(stack):
             fh.write(l)
 
-    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)