forked from mirrors/gecko-dev
Today we don't require that `mach` `CommandProvider`s subclass from any particular parent class and we're very lax about the requirements they must meet. While that's convenient in certain circumstances, it has some unfortunate implications for feature development. Today the only requirements that we have for `CommandProvider`s are that they have an `__init__()` method that takes either 1 or 2 arguments, the second of which must be called `context` and is populated with the `mach` `CommandContext`. Again, while this flexibility is occasionally convenient, it is limiting. As we add features to `mach`, having a better idea what the shape of our `CommandProvider`s are and how we can instantiate them and use them is increasingly important, and this gives us additional control when having `mach` configure `CommandProvider`s based on data that is only available at the `mach` level. In particular, we plan to leverage this in bugs 985141 and 1654074. Here we add validation to the `CommandProvider` decorator to ensure all classes inherit from `MachCommandBase`, update all `CommandProvider`s in-tree to inherit from `MachCommandBase`, and update source and test code accordingly. Follow-up work: we now require (de facto) that the `context` be populated with a `topdir` attribute by the `populate_context_handler` function, since instantiating the `MachCommandBase` requires a `topdir` be provided. This is fine for now in the interest of keeping this patch reasonably sized, but some additional refactoring could make this cleaner. Differential Revision: https://phabricator.services.mozilla.com/D86255
145 lines
5.1 KiB
ReStructuredText
145 lines
5.1 KiB
ReStructuredText
.. _mach_commands:
|
|
|
|
=====================
|
|
Implementing Commands
|
|
=====================
|
|
|
|
Mach commands are defined via Python decorators.
|
|
|
|
All the relevant decorators are defined in the *mach.decorators* module.
|
|
The important decorators are as follows:
|
|
|
|
:py:func:`CommandProvider <mach.decorators.CommandProvider>`
|
|
A class decorator that denotes that a class contains mach
|
|
commands. The decorator takes no arguments.
|
|
|
|
:py:func:`Command <mach.decorators.Command>`
|
|
A method decorator that denotes that the method should be called when
|
|
the specified command is requested. The decorator takes a command name
|
|
as its first argument and a number of additional arguments to
|
|
configure the behavior of the command.
|
|
|
|
:py:func:`CommandArgument <mach.decorators.CommandArgument>`
|
|
A method decorator that defines an argument to the command. Its
|
|
arguments are essentially proxied to ArgumentParser.add_argument()
|
|
|
|
:py:func:`SubCommand <mach.decorators.SubCommand>`
|
|
A method decorator that denotes that the method should be a
|
|
sub-command to an existing ``@Command``. The decorator takes the
|
|
parent command name as its first argument and the sub-command name
|
|
as its second argument.
|
|
|
|
``@CommandArgument`` can be used on ``@SubCommand`` instances just
|
|
like they can on ``@Command`` instances.
|
|
|
|
Classes with the ``@CommandProvider`` decorator **must** subclass
|
|
``MachCommandBase`` and have a compatible ``__init__`` method.
|
|
|
|
Here is a complete example:
|
|
|
|
.. code-block:: python
|
|
|
|
from mach.decorators import (
|
|
CommandArgument,
|
|
CommandProvider,
|
|
Command,
|
|
)
|
|
from mozbuild.base import MachCommandBase
|
|
|
|
@CommandProvider
|
|
class MyClass(MachCommandBase):
|
|
@Command('doit', help='Do ALL OF THE THINGS.')
|
|
@CommandArgument('--force', '-f', action='store_true',
|
|
help='Force doing it.')
|
|
def doit(self, force=False):
|
|
# Do stuff here.
|
|
|
|
When the module is loaded, the decorators tell mach about all handlers.
|
|
When mach runs, it takes the assembled metadata from these handlers and
|
|
hooks it up to the command line driver. Under the hood, arguments passed
|
|
to the decorators are being used to help mach parse command arguments,
|
|
formulate arguments to the methods, etc. See the documentation in the
|
|
:py:mod:`mach.base` module for more.
|
|
|
|
The Python modules defining mach commands do not need to live inside the
|
|
main mach source tree.
|
|
|
|
Conditionally Filtering Commands
|
|
================================
|
|
|
|
Sometimes it might only make sense to run a command given a certain
|
|
context. For example, running tests only makes sense if the product
|
|
they are testing has been built, and said build is available. To make
|
|
sure a command is only runnable from within a correct context, you can
|
|
define a series of conditions on the
|
|
:py:func:`Command <mach.decorators.Command>` decorator.
|
|
|
|
A condition is simply a function that takes an instance of the
|
|
:py:func:`mach.decorators.CommandProvider` class as an argument, and
|
|
returns ``True`` or ``False``. If any of the conditions defined on a
|
|
command return ``False``, the command will not be runnable. The
|
|
docstring of a condition function is used in error messages, to explain
|
|
why the command cannot currently be run.
|
|
|
|
Here is an example:
|
|
|
|
.. code-block:: python
|
|
|
|
from mach.decorators import (
|
|
CommandProvider,
|
|
Command,
|
|
)
|
|
|
|
def build_available(cls):
|
|
"""The build needs to be available."""
|
|
return cls.build_path is not None
|
|
|
|
@CommandProvider
|
|
class MyClass(MachCommandBase):
|
|
def __init__(self, *args, **kwargs):
|
|
super(MyClass, self).__init__(*args, **kwargs)
|
|
self.build_path = ...
|
|
|
|
@Command('run_tests', conditions=[build_available])
|
|
def run_tests(self):
|
|
# Do stuff here.
|
|
|
|
It is important to make sure that any state needed by the condition is
|
|
available to instances of the command provider.
|
|
|
|
By default all commands without any conditions applied will be runnable,
|
|
but it is possible to change this behaviour by setting
|
|
``require_conditions`` to ``True``:
|
|
|
|
.. code-block:: python
|
|
|
|
m = mach.main.Mach()
|
|
m.require_conditions = True
|
|
|
|
Minimizing Code in Commands
|
|
===========================
|
|
|
|
Mach command modules, classes, and methods work best when they are
|
|
minimal dispatchers. The reason is import bloat. Currently, the mach
|
|
core needs to import every Python file potentially containing mach
|
|
commands for every command invocation. If you have dozens of commands or
|
|
commands in modules that import a lot of Python code, these imports
|
|
could slow mach down and waste memory.
|
|
|
|
It is thus recommended that mach modules, classes, and methods do as
|
|
little work as possible. Ideally the module should only import from
|
|
the :py:mod:`mach` package. If you need external modules, you should
|
|
import them from within the command method.
|
|
|
|
To keep code size small, the body of a command method should be limited
|
|
to:
|
|
|
|
1. Obtaining user input (parsing arguments, prompting, etc)
|
|
2. Calling into some other Python package
|
|
3. Formatting output
|
|
|
|
Of course, these recommendations can be ignored if you want to risk
|
|
slower performance.
|
|
|
|
In the future, the mach driver may cache the dispatching information or
|
|
have it intelligently loaded to facilitate lazy loading.
|