diff options
Diffstat (limited to 'buildstream/_frontend/cli.py')
-rw-r--r-- | buildstream/_frontend/cli.py | 129 |
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_): |