summaryrefslogtreecommitdiff
path: root/buildstream/_frontend/cli.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildstream/_frontend/cli.py')
-rw-r--r--buildstream/_frontend/cli.py129
1 files changed, 107 insertions, 22 deletions
diff --git a/buildstream/_frontend/cli.py b/buildstream/_frontend/cli.py
index 41e97cb0e..e59b1baec 100644
--- a/buildstream/_frontend/cli.py
+++ b/buildstream/_frontend/cli.py
@@ -12,6 +12,45 @@ from .complete import main_bashcomplete, complete_path, CompleteUnhandled
# Override of click's main entry point #
##################################################################
+# search_command()
+#
+# Helper function to get a command and context object
+# for a given command.
+#
+# Args:
+# commands (list): A list of command words following `bst` invocation
+# context (click.Context): An existing toplevel context, or None
+#
+# Returns:
+# context (click.Context): The context of the associated command, or None
+#
+def search_command(args, *, context=None):
+ if context is None:
+ context = cli.make_context('bst', args, resilient_parsing=True)
+
+ # Loop into the deepest command
+ command = cli
+ command_ctx = context
+ for cmd in args:
+ command = command_ctx.command.get_command(command_ctx, cmd)
+ if command is None:
+ return None
+ command_ctx = command.make_context(command.name, [command.name],
+ parent=command_ctx,
+ resilient_parsing=True)
+
+ return command_ctx
+
+
+# Completion for completing command names as help arguments
+def complete_commands(cmd, args, incomplete):
+ command_ctx = search_command(args[1:])
+ if command_ctx and command_ctx.command and isinstance(command_ctx.command, click.MultiCommand):
+ return [subcommand + " " for subcommand in command_ctx.command.list_commands(command_ctx)]
+
+ return []
+
+
# Special completion for completing the bst elements in a project dir
def complete_target(args, incomplete):
"""
@@ -20,6 +59,18 @@ def complete_target(args, incomplete):
:return: all the possible user-specified completions for the param
"""
+ project_conf = 'project.conf'
+
+ def ensure_project_dir(directory):
+ directory = os.path.abspath(directory)
+ while not os.path.isfile(os.path.join(directory, project_conf)):
+ parent_dir = os.path.dirname(directory)
+ if directory == parent_dir:
+ break
+ directory = parent_dir
+
+ return directory
+
# First resolve the directory, in case there is an
# active --directory/-C option
#
@@ -35,10 +86,14 @@ def complete_target(args, incomplete):
if idx >= 0 and len(args) > idx + 1:
base_directory = args[idx + 1]
+ else:
+ # Check if this directory or any of its parent directories
+ # contain a project config file
+ base_directory = ensure_project_dir(base_directory)
# Now parse the project.conf just to find the element path,
# this is unfortunately a bit heavy.
- project_file = os.path.join(base_directory, 'project.conf')
+ project_file = os.path.join(base_directory, project_conf)
try:
project = _yaml.load(project_file)
except LoadError:
@@ -57,7 +112,7 @@ def complete_target(args, incomplete):
return complete_path("File", incomplete, base_directory=base_directory)
-def override_completions(cmd_param, args, incomplete):
+def override_completions(cmd, cmd_param, args, incomplete):
"""
:param cmd_param: command definition
:param args: full list of args typed before the incomplete arg
@@ -65,6 +120,9 @@ def override_completions(cmd_param, args, incomplete):
:return: all the possible user-specified completions for the param
"""
+ if cmd.name == 'help':
+ return complete_commands(cmd, args, incomplete)
+
# We can't easily extend click's data structures without
# modifying click itself, so just do some weak special casing
# right here and select which parameters we want to handle specially.
@@ -175,6 +233,33 @@ def cli(context, **kwargs):
##################################################################
+# Help Command #
+##################################################################
+@cli.command(name="help", short_help="Print usage information",
+ context_settings={"help_option_names": []})
+@click.argument("command", nargs=-1, metavar='COMMAND')
+@click.pass_context
+def help_command(ctx, command):
+ """Print usage information about a given command
+ """
+ command_ctx = search_command(command, context=ctx.parent)
+ if not command_ctx:
+ click.echo("Not a valid command: '{} {}'"
+ .format(ctx.parent.info_name, " ".join(command)), err=True)
+ sys.exit(-1)
+
+ click.echo(command_ctx.command.get_help(command_ctx), err=True)
+
+ # Hint about available sub commands
+ if isinstance(command_ctx.command, click.MultiCommand):
+ detail = " "
+ if command:
+ detail = " {} ".format(" ".join(command))
+ click.echo("\nFor usage on a specific command: {} help{}COMMAND"
+ .format(ctx.parent.info_name, detail), err=True)
+
+
+##################################################################
# Init Command #
##################################################################
@cli.command(short_help="Initialize a new BuildStream project")
@@ -206,20 +291,20 @@ def init(app, project_name, format_version, element_path, force):
@click.option('--all', 'all_', default=False, is_flag=True,
help="Build elements that would not be needed for the current build plan")
@click.option('--track', 'track_', multiple=True,
- type=click.Path(dir_okay=False, readable=True),
+ type=click.Path(readable=False),
help="Specify elements to track during the build. Can be used "
"repeatedly to specify multiple elements")
@click.option('--track-all', default=False, is_flag=True,
help="Track all elements in the pipeline")
@click.option('--track-except', multiple=True,
- type=click.Path(dir_okay=False, readable=True),
+ type=click.Path(readable=False),
help="Except certain dependencies from tracking")
@click.option('--track-cross-junctions', '-J', default=False, is_flag=True,
help="Allow tracking to cross junction boundaries")
@click.option('--track-save', default=False, is_flag=True,
help="Deprecated: This is ignored")
@click.argument('elements', nargs=-1,
- type=click.Path(dir_okay=False, readable=True))
+ type=click.Path(readable=False))
@click.pass_obj
def build(app, elements, all_, track_, track_save, track_all, track_except, track_cross_junctions):
"""Build elements in a pipeline"""
@@ -248,7 +333,7 @@ def build(app, elements, all_, track_, track_save, track_all, track_except, trac
##################################################################
@cli.command(short_help="Fetch sources in a pipeline")
@click.option('--except', 'except_', multiple=True,
- type=click.Path(dir_okay=False, readable=True),
+ type=click.Path(readable=False),
help="Except certain dependencies from fetching")
@click.option('--deps', '-d', default='plan',
type=click.Choice(['none', 'plan', 'all']),
@@ -258,7 +343,7 @@ def build(app, elements, all_, track_, track_save, track_all, track_except, trac
@click.option('--track-cross-junctions', '-J', default=False, is_flag=True,
help="Allow tracking to cross junction boundaries")
@click.argument('elements', nargs=-1,
- type=click.Path(dir_okay=False, readable=True))
+ type=click.Path(readable=False))
@click.pass_obj
def fetch(app, elements, deps, track_, except_, track_cross_junctions):
"""Fetch sources required to build the pipeline
@@ -299,7 +384,7 @@ def fetch(app, elements, deps, track_, except_, track_cross_junctions):
##################################################################
@cli.command(short_help="Track new source references")
@click.option('--except', 'except_', multiple=True,
- type=click.Path(dir_okay=False, readable=True),
+ type=click.Path(readable=False),
help="Except certain dependencies from tracking")
@click.option('--deps', '-d', default='none',
type=click.Choice(['none', 'all']),
@@ -307,7 +392,7 @@ def fetch(app, elements, deps, track_, except_, track_cross_junctions):
@click.option('--cross-junctions', '-J', default=False, is_flag=True,
help="Allow crossing junction boundaries")
@click.argument('elements', nargs=-1,
- type=click.Path(dir_okay=False, readable=True))
+ type=click.Path(readable=False))
@click.pass_obj
def track(app, elements, deps, except_, cross_junctions):
"""Consults the specified tracking branches for new versions available
@@ -339,7 +424,7 @@ def track(app, elements, deps, except_, cross_junctions):
@click.option('--remote', '-r',
help="The URL of the remote cache (defaults to the first configured cache)")
@click.argument('elements', nargs=-1,
- type=click.Path(dir_okay=False, readable=True))
+ type=click.Path(readable=False))
@click.pass_obj
def pull(app, elements, deps, remote):
"""Pull a built artifact from the configured remote artifact cache.
@@ -368,7 +453,7 @@ def pull(app, elements, deps, remote):
@click.option('--remote', '-r', default=None,
help="The URL of the remote cache (defaults to the first configured cache)")
@click.argument('elements', nargs=-1,
- type=click.Path(dir_okay=False, readable=True))
+ type=click.Path(readable=False))
@click.pass_obj
def push(app, elements, deps, remote):
"""Push a built artifact to a remote artifact cache.
@@ -391,7 +476,7 @@ def push(app, elements, deps, remote):
##################################################################
@cli.command(short_help="Show elements in the pipeline")
@click.option('--except', 'except_', multiple=True,
- type=click.Path(dir_okay=False, readable=True),
+ type=click.Path(readable=False),
help="Except certain dependencies")
@click.option('--deps', '-d', default='all',
type=click.Choice(['none', 'plan', 'run', 'build', 'all']),
@@ -403,7 +488,7 @@ def push(app, elements, deps, remote):
type=click.STRING,
help='Format string for each element')
@click.argument('elements', nargs=-1,
- type=click.Path(dir_okay=False, readable=True))
+ type=click.Path(readable=False))
@click.pass_obj
def show(app, elements, deps, except_, order, format_):
"""Show elements in the pipeline
@@ -482,7 +567,7 @@ def show(app, elements, deps, except_, order, format_):
@click.option('--isolate', is_flag=True, default=False,
help='Create an isolated build sandbox')
@click.argument('element',
- type=click.Path(dir_okay=False, readable=True))
+ type=click.Path(readable=False))
@click.argument('command', type=click.STRING, nargs=-1)
@click.pass_obj
def shell(app, element, sysroot, mount, isolate, build_, command):
@@ -543,7 +628,7 @@ def shell(app, element, sysroot, mount, isolate, build_, command):
@click.option('--hardlinks', default=False, is_flag=True,
help="Checkout hardlinks instead of copies (handle with care)")
@click.argument('element',
- type=click.Path(dir_okay=False, readable=True))
+ type=click.Path(readable=False))
@click.argument('directory', type=click.Path(file_okay=False))
@click.pass_obj
def checkout(app, element, directory, force, integrate, hardlinks):
@@ -577,7 +662,7 @@ def workspace():
@click.option('--track', 'track_', default=False, is_flag=True,
help="Track and fetch new source references before checking out the workspace")
@click.argument('element',
- type=click.Path(dir_okay=False, readable=True))
+ type=click.Path(readable=False))
@click.argument('directory', type=click.Path(file_okay=False))
@click.pass_obj
def workspace_open(app, no_checkout, force, track_, element, directory):
@@ -609,7 +694,7 @@ def workspace_open(app, no_checkout, force, track_, element, directory):
@click.option('--all', '-a', 'all_', default=False, is_flag=True,
help="Close all open workspaces")
@click.argument('elements', nargs=-1,
- type=click.Path(dir_okay=False, readable=True))
+ type=click.Path(readable=False))
@click.pass_obj
def workspace_close(app, remove_dir, all_, elements):
"""Close a workspace"""
@@ -626,7 +711,7 @@ def workspace_close(app, remove_dir, all_, elements):
sys.exit(0)
if all_:
- elements = [element_name for element_name, _ in app.project.workspaces.list()]
+ elements = [element_name for element_name, _ in app.context.get_workspaces().list()]
elements = app.stream.redirect_element_names(elements)
@@ -658,7 +743,7 @@ def workspace_close(app, remove_dir, all_, elements):
@click.option('--all', '-a', 'all_', default=False, is_flag=True,
help="Reset all open workspaces")
@click.argument('elements', nargs=-1,
- type=click.Path(dir_okay=False, readable=True))
+ type=click.Path(readable=False))
@click.pass_obj
def workspace_reset(app, soft, track_, all_, elements):
"""Reset a workspace to its original state"""
@@ -678,7 +763,7 @@ def workspace_reset(app, soft, track_, all_, elements):
sys.exit(-1)
if all_:
- elements = tuple(element_name for element_name, _ in app.project.workspaces.list())
+ elements = tuple(element_name for element_name, _ in app.context.get_workspaces().list())
app.stream.workspace_reset(elements, soft=soft, track_first=track_)
@@ -700,7 +785,7 @@ def workspace_list(app):
##################################################################
@cli.command(name="source-bundle", short_help="Produce a build bundle to be manually executed")
@click.option('--except', 'except_', multiple=True,
- type=click.Path(dir_okay=False, readable=True),
+ type=click.Path(readable=False),
help="Elements to except from the tarball")
@click.option('--compression', default='gz',
type=click.Choice(['none', 'gz', 'bz2', 'xz']),
@@ -712,7 +797,7 @@ def workspace_list(app):
@click.option('--directory', default=os.getcwd(),
help="The directory to write the tarball to")
@click.argument('element',
- type=click.Path(dir_okay=False, readable=True))
+ type=click.Path(readable=False))
@click.pass_obj
def source_bundle(app, element, force, directory,
track_, compression, except_):