diff --git a/src/python/pants/engine/goal.py b/src/python/pants/engine/goal.py index e91f2bc1b375..7be504a9603a 100644 --- a/src/python/pants/engine/goal.py +++ b/src/python/pants/engine/goal.py @@ -1,15 +1,19 @@ # coding=utf-8 -# Copyright 2018 Pants project contributors (see CONTRIBUTORS.md). +# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). from __future__ import absolute_import, division, print_function, unicode_literals +from contextlib import contextmanager + +from pants.cache.cache_setup import CacheSetup from pants.option.optionable import Optionable from pants.option.scope import ScopeInfo +from pants.subsystem.subsystem_client_mixin import SubsystemClientMixin from pants.util.meta import AbstractClass, classproperty -class Goal(Optionable, AbstractClass): +class Goal(SubsystemClientMixin, Optionable, AbstractClass): """A CLI goal whch is implemented by a `@console_rule`. This abstract class should be subclassed and given a `Goal.name` that it will be referred to by @@ -19,7 +23,10 @@ class Goal(Optionable, AbstractClass): # Subclasser-defined. See the class pydoc. name = None - options_scope_category = ScopeInfo.GOAL + # If this Goal should have an associated deprecated instance of `CacheSetup` (which was implicitly + # required by all v1 Tasks), subclasses may set this to a valid deprecation version to create + # that association. + deprecated_cache_setup_removal_version = None @classproperty def options_scope(cls): @@ -29,12 +36,25 @@ def options_scope(cls): return cls.name @classmethod - def subsystem_dependencies_iter(cls): - # NB: `Goal` quacks like a `SubsystemClientMixin` in order to allow v1 `Tasks` to depend on - # v2 Goals for backwards compatibility purposes. But v2 Goals should _not_ have subsystem - # dependencies: instead, the @rules participating (transitively) in a Goal should directly - # declare Subsystem deps. - return iter([]) + def subsystem_dependencies(cls): + # NB: `Goal` implements `SubsystemClientMixin` in order to allow v1 `Tasks` to depend on + # v2 Goals, and for `Goals` to declare a deprecated dependency on a `CacheSetup` instance for + # backwards compatibility purposes. But v2 Goals should _not_ have subsystem dependencies: + # instead, the @rules participating (transitively) in a Goal should directly declare + # Subsystem deps. + if cls.deprecated_cache_setup_removal_version: + dep = CacheSetup.scoped( + cls, + removal_version=cls.deprecated_cache_setup_removal_version, + removal_hint='Goal `{}` uses an independent caching implementation, and ignores `{}`.'.format( + cls.name, + CacheSetup.subscope(cls.name), + ) + ) + return (dep,) + return tuple() + + options_scope_category = ScopeInfo.GOAL def __init__(self, scope, scoped_options): # NB: This constructor is shaped to meet the contract of `Optionable(Factory).signature`. @@ -46,3 +66,37 @@ def __init__(self, scope, scoped_options): def options(self): """Returns the option values for this Goal.""" return self._scoped_options + + +class LineOriented(object): + """A mixin for Goal that adds options and a context manager for line-oriented output.""" + + @classmethod + def register_options(cls, register): + super(LineOriented, cls).register_options(register) + register('--sep', default='\\n', metavar='', + help='String to use to separate result lines.') + register('--output-file', metavar='', + help='Write line-oriented output to this file instead.') + + @contextmanager + def line_oriented(self, console): + """Takes a Console and yields functions for writing to stdout and stderr, respectively.""" + + output_file = self.options.output_file + sep = self.options.sep.encode('utf-8').decode('unicode_escape') + + stdout, stderr = console.stdout, console.stderr + if output_file: + stdout = open(output_file, 'w') + + try: + print_stdout = lambda msg: print(msg, file=stdout, end=sep) + print_stderr = lambda msg: print(msg, file=stderr) + yield print_stdout, print_stderr + finally: + if output_file: + stdout.close() + else: + stdout.flush() + stderr.flush() diff --git a/src/python/pants/rules/core/list_targets.py b/src/python/pants/rules/core/list_targets.py index 53cdf345d40a..9c50df9648b9 100644 --- a/src/python/pants/rules/core/list_targets.py +++ b/src/python/pants/rules/core/list_targets.py @@ -1,5 +1,5 @@ # coding=utf-8 -# Copyright 2018 Pants project contributors (see CONTRIBUTORS.md). +# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). from __future__ import absolute_import, division, print_function, unicode_literals @@ -7,17 +7,19 @@ from pants.base.specs import Specs from pants.engine.addressable import BuildFileAddresses from pants.engine.console import Console -from pants.engine.goal import Goal +from pants.engine.goal import Goal, LineOriented from pants.engine.legacy.graph import HydratedTargets from pants.engine.rules import console_rule from pants.engine.selectors import Get -class List(Goal): +class List(LineOriented, Goal): """Lists all targets matching the target specs.""" name = 'list' + deprecated_cache_setup_removal_version = '1.18.0.dev2' + @classmethod def register_options(cls, register): super(List, cls).register_options(register) @@ -71,13 +73,14 @@ def print_documented(target): collection = yield Get(BuildFileAddresses, Specs, specs) print_fn = lambda address: address.spec - if not collection.dependencies: - console.print_stderr('WARNING: No targets were matched in goal `{}`.'.format('list')) + with list_goal.line_oriented(console) as (print_stdout, print_stderr): + if not collection.dependencies: + print_stderr('WARNING: No targets were matched in goal `{}`.'.format('list')) - for item in collection: - result = print_fn(item) - if result: - console.print_stdout(result) + for item in collection: + result = print_fn(item) + if result: + print_stdout(result) def rules():