From 325ac03082cb499f0e9b6585888a29647cfe890a Mon Sep 17 00:00:00 2001 From: Jonathan Maw <jonathan.maw@codethink.co.uk> Date: Mon, 19 Nov 2018 17:26:59 +0000 Subject: [PATCH] WIP: Make specifying elements optional in bst commands Known issues: * `bst shell` works, but `bst shell COMMANDS...` doesn't, because click has no way of separating optional args from variable-length args. * `bst checkout`'s usage string marks LOCATION as an optional argument. Because click gets confused if there's an optional argument before a mandatory argument, I had to mark LOCATION as optional internally. * `bst workspace open` makes no sense with element being optional, so I skipped it. * `bst workspace close` will probably need to be revisited when multiple projects can own one workspace. * `bst workspace reset` will happily delete the directory you're currently in, requiring you to `cd $PWD` to see the contents of your directory. I could exclude the top-level directory of the workspace being deleted, but it is entirely valid to run workspace commands from deeper in the workspace. * `bst source-bundle` does not work if a workspace is open at all, and according to #672 is scoped for deprecation, so I have left it alone. --- buildstream/_context.py | 8 +++- buildstream/_frontend/app.py | 18 ++++++++- buildstream/_frontend/cli.py | 78 +++++++++++++++++++++++++++++++++--- 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/buildstream/_context.py b/buildstream/_context.py index 57a4749217..ef4a7a1de8 100644 --- a/buildstream/_context.py +++ b/buildstream/_context.py @@ -47,9 +47,13 @@ from .plugin import _plugin_lookup # verbosity levels and basically anything pertaining to the context # in which BuildStream was invoked. # +# Args: +# workspace_project_cache (WorkspaceProjectCache): A WorkspaceProjectCache +# for this invocation +# class Context(): - def __init__(self): + def __init__(self, workspace_project_cache=None): # Filename indicating which configuration file was used, or None for the defaults self.config_origin = None @@ -144,7 +148,7 @@ class Context(): self._projects = [] self._project_overrides = {} self._workspaces = None - self._workspace_project_cache = WorkspaceProjectCache() + self._workspace_project_cache = workspace_project_cache or WorkspaceProjectCache() self._log_handle = None self._log_filename = None self._cascache = None diff --git a/buildstream/_frontend/app.py b/buildstream/_frontend/app.py index 4094eec17b..e3a7603945 100644 --- a/buildstream/_frontend/app.py +++ b/buildstream/_frontend/app.py @@ -39,6 +39,7 @@ from .._stream import Stream from .._versions import BST_FORMAT_VERSION from .. import _yaml from .._scheduler import ElementJob +from .._workspaces import WorkspaceProjectCache # Import frontend assets from . import Profile, LogLine, Status @@ -79,6 +80,7 @@ class App(): self._fail_messages = {} # Failure messages by unique plugin id self._interactive_failures = None # Whether to handle failures interactively self._started = False # Whether a session has started + self._workspace_project_cache = WorkspaceProjectCache() # A collection of workspace local data # UI Colors Profiles self._content_profile = Profile(fg='yellow') @@ -164,7 +166,7 @@ class App(): # Load the Context # try: - self.context = Context() + self.context = Context(self._workspace_project_cache) self.context.load(config) except BstError as e: self._error_exit(e, "Error loading user configuration") @@ -402,6 +404,20 @@ class App(): if self.stream: self.stream.cleanup() + # guess_element() + # + # Attempts to interpret which element the user intended to run commands on + # + # Returns: + # (str) The name of the element, or an empty string + def guess_element(self): + directory = self._main_options['directory'] + workspace_project = self._workspace_project_cache.get(directory) + if workspace_project: + return workspace_project.get_default_element() + else: + return "" + ############################################################ # Abstract Class Methods # ############################################################ diff --git a/buildstream/_frontend/cli.py b/buildstream/_frontend/cli.py index 04befb3fdd..0d22f87dfd 100644 --- a/buildstream/_frontend/cli.py +++ b/buildstream/_frontend/cli.py @@ -312,6 +312,12 @@ def build(app, elements, all_, track_, track_save, track_all, track_except, trac if track_save: click.echo("WARNING: --track-save is deprecated, saving is now unconditional", err=True) + if not all_ and not elements: + # Attempt to divine the element from the workspace you're in + guessed_target = app.guess_element() + if guessed_target: + elements = (guessed_target,) + if track_all: track_ = elements @@ -366,6 +372,12 @@ def fetch(app, elements, deps, track_, except_, track_cross_junctions): "Since tracking modifies the build plan, all elements will be tracked.", err=True) deps = PipelineSelection.ALL + if not elements: + # Attempt to divine the element from the workspace you're in + guessed_target = app.guess_element() + if guessed_target: + elements = (guessed_target,) + with app.initialized(session_name="Fetch"): app.stream.fetch(elements, selection=deps, @@ -402,6 +414,12 @@ def track(app, elements, deps, except_, cross_junctions): none: No dependencies, just the specified elements all: All dependencies of all specified elements """ + if not elements: + # Attempt to divine the element from the workspace you're in + guessed_target = app.guess_element() + if guessed_target: + elements = (guessed_target,) + with app.initialized(session_name="Track"): # Substitute 'none' for 'redirect' so that element redirections # will be done @@ -438,6 +456,12 @@ def pull(app, elements, deps, remote): none: No dependencies, just the element itself all: All dependencies """ + if not elements: + # Attempt to divine the element from the workspace you're in + guessed_target = app.guess_element() + if guessed_target: + elements = (guessed_target,) + with app.initialized(session_name="Pull"): app.stream.pull(elements, selection=deps, remote=remote) @@ -466,6 +490,11 @@ def push(app, elements, deps, remote): none: No dependencies, just the element itself all: All dependencies """ + if not elements: + # Attempt to divine the element from the workspace you're in + guessed_target = app.guess_element() + if guessed_target: + elements = (guessed_target,) with app.initialized(session_name="Push"): app.stream.push(elements, selection=deps, remote=remote) @@ -536,6 +565,12 @@ def show(app, elements, deps, except_, order, format_): bst show target.bst --format \\ $'---------- %{name} ----------\\n%{vars}' """ + if not elements: + # Attempt to divine the element from the workspace you're in + guessed_target = app.guess_element() + if guessed_target: + elements = (guessed_target,) + with app.initialized(): dependencies = app.stream.load_selection(elements, selection=deps, @@ -565,7 +600,7 @@ def show(app, elements, deps, except_, order, format_): help="Mount a file or directory into the sandbox") @click.option('--isolate', is_flag=True, default=False, help='Create an isolated build sandbox') -@click.argument('element', +@click.argument('element', required=False, type=click.Path(readable=False)) @click.argument('command', type=click.STRING, nargs=-1) @click.pass_obj @@ -596,6 +631,14 @@ def shell(app, element, sysroot, mount, isolate, build_, command): scope = Scope.RUN with app.initialized(): + if not element: + # Attempt to divine the element from the workspace you're in + guessed_target = app.guess_element() + if guessed_target: + element = guessed_target + else: + raise AppError('Error: Missing argument "ELEMENT".') + dependencies = app.stream.load_selection((element,), selection=PipelineSelection.NONE) element = dependencies[0] prompt = app.shell_prompt(element) @@ -633,14 +676,27 @@ def shell(app, element, sysroot, mount, isolate, build_, command): help="Create a tarball from the artifact contents instead " "of a file tree. If LOCATION is '-', the tarball " "will be dumped to the standard output.") -@click.argument('element', +@click.argument('element', required=False, type=click.Path(readable=False)) -@click.argument('location', type=click.Path()) +@click.argument('location', type=click.Path(), required=False) @click.pass_obj def checkout(app, element, location, force, deps, integrate, hardlinks, tar): """Checkout a built artifact to the specified location """ + if not element and not location: + click.echo("ERROR: LOCATION is not specified", err=True) + sys.exit(-1) + + if element and not location: + # Nasty hack to get around click's optional args + location = element + element = app.guess_element() + + if not element: + click.echo("ERROR: ELEMENT is not specified", err=True) + sys.exit(-1) + if hardlinks and tar: click.echo("ERROR: options --hardlinks and --tar conflict", err=True) sys.exit(-1) @@ -732,8 +788,14 @@ def workspace_close(app, remove_dir, all_, elements): """Close a workspace""" if not (all_ or elements): - click.echo('ERROR: no elements specified', err=True) - sys.exit(-1) + # NOTE: I may need to revisit this when implementing multiple projects + # opening one workspace. + element = app.guess_element() + if element: + elements = (element,) + else: + click.echo('ERROR: no elements specified', err=True) + sys.exit(-1) with app.initialized(): @@ -791,7 +853,11 @@ def workspace_reset(app, soft, track_, all_, elements): with app.initialized(): if not (all_ or elements): - raise AppError('No elements specified to reset') + element = app.guess_element() + if element: + elements = (element,) + else: + raise AppError('No elements specified to reset') if all_ and not app.stream.workspace_exists(): raise AppError("No open workspaces to reset") -- GitLab