1import os 2import sys 3import fcntl 4 5import click 6from .. import _yaml 7from .._exceptions import BstError, LoadError, AppError 8from .._versions import BST_FORMAT_VERSION 9from .complete import main_bashcomplete, complete_path, CompleteUnhandled 10 11 12################################################################## 13# Override of click's main entry point # 14################################################################## 15 16# search_command() 17# 18# Helper function to get a command and context object 19# for a given command. 20# 21# Args: 22# commands (list): A list of command words following `bst` invocation 23# context (click.Context): An existing toplevel context, or None 24# 25# Returns: 26# context (click.Context): The context of the associated command, or None 27# 28def search_command(args, *, context=None): 29 if context is None: 30 context = cli.make_context('bst', args, resilient_parsing=True) 31 32 # Loop into the deepest command 33 command = cli 34 command_ctx = context 35 for cmd in args: 36 command = command_ctx.command.get_command(command_ctx, cmd) 37 if command is None: 38 return None 39 command_ctx = command.make_context(command.name, [command.name], 40 parent=command_ctx, 41 resilient_parsing=True) 42 43 return command_ctx 44 45 46# Completion for completing command names as help arguments 47def complete_commands(cmd, args, incomplete): 48 command_ctx = search_command(args[1:]) 49 if command_ctx and command_ctx.command and isinstance(command_ctx.command, click.MultiCommand): 50 return [subcommand + " " for subcommand in command_ctx.command.list_commands(command_ctx)] 51 52 return [] 53 54 55# Special completion for completing the bst elements in a project dir 56def complete_target(args, incomplete): 57 """ 58 :param args: full list of args typed before the incomplete arg 59 :param incomplete: the incomplete text to autocomplete 60 :return: all the possible user-specified completions for the param 61 """ 62 63 project_conf = 'project.conf' 64 65 def ensure_project_dir(directory): 66 directory = os.path.abspath(directory) 67 while not os.path.isfile(os.path.join(directory, project_conf)): 68 parent_dir = os.path.dirname(directory) 69 if directory == parent_dir: 70 break 71 directory = parent_dir 72 73 return directory 74 75 # First resolve the directory, in case there is an 76 # active --directory/-C option 77 # 78 base_directory = '.' 79 idx = -1 80 try: 81 idx = args.index('-C') 82 except ValueError: 83 try: 84 idx = args.index('--directory') 85 except ValueError: 86 pass 87 88 if idx >= 0 and len(args) > idx + 1: 89 base_directory = args[idx + 1] 90 else: 91 # Check if this directory or any of its parent directories 92 # contain a project config file 93 base_directory = ensure_project_dir(base_directory) 94 95 # Now parse the project.conf just to find the element path, 96 # this is unfortunately a bit heavy. 97 project_file = os.path.join(base_directory, project_conf) 98 try: 99 project = _yaml.load(project_file) 100 except LoadError: 101 # If there is no project directory in context, just dont 102 # even bother trying to complete anything. 103 return [] 104 105 # The project is not required to have an element-path 106 element_directory = project.get('element-path') 107 108 # If a project was loaded, use it's element-path to 109 # adjust our completion's base directory 110 if element_directory: 111 base_directory = os.path.join(base_directory, element_directory) 112 113 return complete_path("File", incomplete, base_directory=base_directory) 114 115 116def override_completions(cmd, cmd_param, args, incomplete): 117 """ 118 :param cmd_param: command definition 119 :param args: full list of args typed before the incomplete arg 120 :param incomplete: the incomplete text to autocomplete 121 :return: all the possible user-specified completions for the param 122 """ 123 124 if cmd.name == 'help': 125 return complete_commands(cmd, args, incomplete) 126 127 # We can't easily extend click's data structures without 128 # modifying click itself, so just do some weak special casing 129 # right here and select which parameters we want to handle specially. 130 if isinstance(cmd_param.type, click.Path) and \ 131 (cmd_param.name == 'elements' or 132 cmd_param.name == 'element' or 133 cmd_param.name == 'except_' or 134 cmd_param.opts == ['--track'] or 135 cmd_param.opts == ['--track-except']): 136 return complete_target(args, incomplete) 137 138 raise CompleteUnhandled() 139 140 141def override_main(self, args=None, prog_name=None, complete_var=None, 142 standalone_mode=True, **extra): 143 144 # Hook for the Bash completion. This only activates if the Bash 145 # completion is actually enabled, otherwise this is quite a fast 146 # noop. 147 if main_bashcomplete(self, prog_name, override_completions): 148 149 # If we're running tests we cant just go calling exit() 150 # from the main process. 151 # 152 # The below is a quicker exit path for the sake 153 # of making completions respond faster. 154 if 'BST_TEST_SUITE' not in os.environ: 155 sys.stdout.flush() 156 sys.stderr.flush() 157 os._exit(0) 158 159 # Regular client return for test cases 160 return 161 162 # Check output file descriptor at earliest opportunity, to 163 # provide a reasonable error message instead of a stack trace 164 # in the case that it is blocking 165 for stream in (sys.stdout, sys.stderr): 166 fileno = stream.fileno() 167 flags = fcntl.fcntl(fileno, fcntl.F_GETFL) 168 if flags & os.O_NONBLOCK: 169 click.echo("{} is currently set to O_NONBLOCK, try opening a new shell" 170 .format(stream.name), err=True) 171 sys.exit(-1) 172 173 original_main(self, args=args, prog_name=prog_name, complete_var=None, 174 standalone_mode=standalone_mode, **extra) 175 176 177original_main = click.BaseCommand.main 178click.BaseCommand.main = override_main 179 180 181################################################################## 182# Main Options # 183################################################################## 184def print_version(ctx, param, value): 185 if not value or ctx.resilient_parsing: 186 return 187 188 from .. import __version__ 189 click.echo(__version__) 190 ctx.exit() 191 192 193@click.group(context_settings=dict(help_option_names=['-h', '--help'])) 194@click.option('--version', is_flag=True, callback=print_version, 195 expose_value=False, is_eager=True) 196@click.option('--config', '-c', 197 type=click.Path(exists=True, dir_okay=False, readable=True), 198 help="Configuration file to use") 199@click.option('--directory', '-C', default=os.getcwd(), 200 type=click.Path(file_okay=False, readable=True), 201 help="Project directory (default: current directory)") 202@click.option('--on-error', default=None, 203 type=click.Choice(['continue', 'quit', 'terminate']), 204 help="What to do when an error is encountered") 205@click.option('--fetchers', type=click.INT, default=None, 206 help="Maximum simultaneous download tasks") 207@click.option('--builders', type=click.INT, default=None, 208 help="Maximum simultaneous build tasks") 209@click.option('--pushers', type=click.INT, default=None, 210 help="Maximum simultaneous upload tasks") 211@click.option('--network-retries', type=click.INT, default=None, 212 help="Maximum retries for network tasks") 213@click.option('--no-interactive', is_flag=True, default=False, 214 help="Force non interactive mode, otherwise this is automatically decided") 215@click.option('--verbose/--no-verbose', default=None, 216 help="Be extra verbose") 217@click.option('--debug/--no-debug', default=None, 218 help="Print debugging output") 219@click.option('--error-lines', type=click.INT, default=None, 220 help="Maximum number of lines to show from a task log") 221@click.option('--message-lines', type=click.INT, default=None, 222 help="Maximum number of lines to show in a detailed message") 223@click.option('--log-file', 224 type=click.File(mode='w', encoding='UTF-8'), 225 help="A file to store the main log (allows storing the main log while in interactive mode)") 226@click.option('--colors/--no-colors', default=None, 227 help="Force enable/disable ANSI color codes in output") 228@click.option('--strict/--no-strict', default=None, is_flag=True, 229 help="Elements must be rebuilt when their dependencies have changed") 230@click.option('--option', '-o', type=click.Tuple([str, str]), multiple=True, metavar='OPTION VALUE', 231 help="Specify a project option") 232@click.option('--default-mirror', default=None, 233 help="The mirror to fetch from first, before attempting other mirrors") 234@click.pass_context 235def cli(context, **kwargs): 236 """Build and manipulate BuildStream projects 237 238 Most of the main options override options in the 239 user preferences configuration file. 240 """ 241 242 from .app import App 243 244 # Create the App, giving it the main arguments 245 context.obj = App.create(dict(kwargs)) 246 context.call_on_close(context.obj.cleanup) 247 248 249################################################################## 250# Help Command # 251################################################################## 252@cli.command(name="help", short_help="Print usage information", 253 context_settings={"help_option_names": []}) 254@click.argument("command", nargs=-1, metavar='COMMAND') 255@click.pass_context 256def help_command(ctx, command): 257 """Print usage information about a given command 258 """ 259 command_ctx = search_command(command, context=ctx.parent) 260 if not command_ctx: 261 click.echo("Not a valid command: '{} {}'" 262 .format(ctx.parent.info_name, " ".join(command)), err=True) 263 sys.exit(-1) 264 265 click.echo(command_ctx.command.get_help(command_ctx), err=True) 266 267 # Hint about available sub commands 268 if isinstance(command_ctx.command, click.MultiCommand): 269 detail = " " 270 if command: 271 detail = " {} ".format(" ".join(command)) 272 click.echo("\nFor usage on a specific command: {} help{}COMMAND" 273 .format(ctx.parent.info_name, detail), err=True) 274 275 276################################################################## 277# Init Command # 278################################################################## 279@cli.command(short_help="Initialize a new BuildStream project") 280@click.option('--project-name', type=click.STRING, 281 help="The project name to use") 282@click.option('--format-version', type=click.INT, default=BST_FORMAT_VERSION, 283 help="The required format version (default: {})".format(BST_FORMAT_VERSION)) 284@click.option('--element-path', type=click.Path(), default="elements", 285 help="The subdirectory to store elements in (default: elements)") 286@click.option('--force', '-f', default=False, is_flag=True, 287 help="Allow overwriting an existing project.conf") 288@click.pass_obj 289def init(app, project_name, format_version, element_path, force): 290 """Initialize a new BuildStream project 291 292 Creates a new BuildStream project.conf in the project 293 directory. 294 295 Unless `--project-name` is specified, this will be an 296 interactive session. 297 """ 298 app.init_project(project_name, format_version, element_path, force) 299 300 301################################################################## 302# Build Command # 303################################################################## 304@cli.command(short_help="Build elements in a pipeline") 305@click.option('--all', 'all_', default=False, is_flag=True, 306 help="Build elements that would not be needed for the current build plan") 307@click.option('--track', 'track_', multiple=True, 308 type=click.Path(readable=False), 309 help="Specify elements to track during the build. Can be used " 310 "repeatedly to specify multiple elements") 311@click.option('--track-all', default=False, is_flag=True, 312 help="Track all elements in the pipeline") 313@click.option('--track-except', multiple=True, 314 type=click.Path(readable=False), 315 help="Except certain dependencies from tracking") 316@click.option('--track-cross-junctions', '-J', default=False, is_flag=True, 317 help="Allow tracking to cross junction boundaries") 318@click.option('--track-save', default=False, is_flag=True, 319 help="Deprecated: This is ignored") 320@click.argument('elements', nargs=-1, 321 type=click.Path(readable=False)) 322@click.pass_obj 323def build(app, elements, all_, track_, track_save, track_all, track_except, track_cross_junctions): 324 """Build elements in a pipeline""" 325 326 if (track_except or track_cross_junctions) and not (track_ or track_all): 327 click.echo("ERROR: The --track-except and --track-cross-junctions options " 328 "can only be used with --track or --track-all", err=True) 329 sys.exit(-1) 330 331 if track_save: 332 click.echo("WARNING: --track-save is deprecated, saving is now unconditional", err=True) 333 334 if track_all: 335 track_ = elements 336 337 with app.initialized(session_name="Build"): 338 app.stream.build(elements, 339 track_targets=track_, 340 track_except=track_except, 341 track_cross_junctions=track_cross_junctions, 342 build_all=all_) 343 344 345################################################################## 346# Fetch Command # 347################################################################## 348@cli.command(short_help="Fetch sources in a pipeline") 349@click.option('--except', 'except_', multiple=True, 350 type=click.Path(readable=False), 351 help="Except certain dependencies from fetching") 352@click.option('--deps', '-d', default='plan', 353 type=click.Choice(['none', 'plan', 'all']), 354 help='The dependencies to fetch (default: plan)') 355@click.option('--track', 'track_', default=False, is_flag=True, 356 help="Track new source references before fetching") 357@click.option('--track-cross-junctions', '-J', default=False, is_flag=True, 358 help="Allow tracking to cross junction boundaries") 359@click.argument('elements', nargs=-1, 360 type=click.Path(readable=False)) 361@click.pass_obj 362def fetch(app, elements, deps, track_, except_, track_cross_junctions): 363 """Fetch sources required to build the pipeline 364 365 By default this will only try to fetch sources which are 366 required for the build plan of the specified target element, 367 omitting sources for any elements which are already built 368 and available in the artifact cache. 369 370 Specify `--deps` to control which sources to fetch: 371 372 \b 373 none: No dependencies, just the element itself 374 plan: Only dependencies required for the build plan 375 all: All dependencies 376 """ 377 from .._pipeline import PipelineSelection 378 379 if track_cross_junctions and not track_: 380 click.echo("ERROR: The --track-cross-junctions option can only be used with --track", err=True) 381 sys.exit(-1) 382 383 if track_ and deps == PipelineSelection.PLAN: 384 click.echo("WARNING: --track specified for tracking of a build plan\n\n" 385 "Since tracking modifies the build plan, all elements will be tracked.", err=True) 386 deps = PipelineSelection.ALL 387 388 with app.initialized(session_name="Fetch"): 389 app.stream.fetch(elements, 390 selection=deps, 391 except_targets=except_, 392 track_targets=track_, 393 track_cross_junctions=track_cross_junctions) 394 395 396################################################################## 397# Track Command # 398################################################################## 399@cli.command(short_help="Track new source references") 400@click.option('--except', 'except_', multiple=True, 401 type=click.Path(readable=False), 402 help="Except certain dependencies from tracking") 403@click.option('--deps', '-d', default='none', 404 type=click.Choice(['none', 'all']), 405 help='The dependencies to track (default: none)') 406@click.option('--cross-junctions', '-J', default=False, is_flag=True, 407 help="Allow crossing junction boundaries") 408@click.argument('elements', nargs=-1, 409 type=click.Path(readable=False)) 410@click.pass_obj 411def track(app, elements, deps, except_, cross_junctions): 412 """Consults the specified tracking branches for new versions available 413 to build and updates the project with any newly available references. 414 415 By default this will track just the specified element, but you can also 416 update a whole tree of dependencies in one go. 417 418 Specify `--deps` to control which sources to track: 419 420 \b 421 none: No dependencies, just the specified elements 422 all: All dependencies of all specified elements 423 """ 424 with app.initialized(session_name="Track"): 425 # Substitute 'none' for 'redirect' so that element redirections 426 # will be done 427 if deps == 'none': 428 deps = 'redirect' 429 app.stream.track(elements, 430 selection=deps, 431 except_targets=except_, 432 cross_junctions=cross_junctions) 433 434 435################################################################## 436# Pull Command # 437################################################################## 438@cli.command(short_help="Pull a built artifact") 439@click.option('--deps', '-d', default='none', 440 type=click.Choice(['none', 'all']), 441 help='The dependency artifacts to pull (default: none)') 442@click.option('--remote', '-r', 443 help="The URL of the remote cache (defaults to the first configured cache)") 444@click.argument('elements', nargs=-1, 445 type=click.Path(readable=False)) 446@click.pass_obj 447def pull(app, elements, deps, remote): 448 """Pull a built artifact from the configured remote artifact cache. 449 450 By default the artifact will be pulled one of the configured caches 451 if possible, following the usual priority order. If the `--remote` flag 452 is given, only the specified cache will be queried. 453 454 Specify `--deps` to control which artifacts to pull: 455 456 \b 457 none: No dependencies, just the element itself 458 all: All dependencies 459 """ 460 with app.initialized(session_name="Pull"): 461 app.stream.pull(elements, selection=deps, remote=remote) 462 463 464################################################################## 465# Push Command # 466################################################################## 467@cli.command(short_help="Push a built artifact") 468@click.option('--deps', '-d', default='none', 469 type=click.Choice(['none', 'all']), 470 help='The dependencies to push (default: none)') 471@click.option('--remote', '-r', default=None, 472 help="The URL of the remote cache (defaults to the first configured cache)") 473@click.argument('elements', nargs=-1, 474 type=click.Path(readable=False)) 475@click.pass_obj 476def push(app, elements, deps, remote): 477 """Push a built artifact to a remote artifact cache. 478 479 The default destination is the highest priority configured cache. You can 480 override this by passing a different cache URL with the `--remote` flag. 481 482 Specify `--deps` to control which artifacts to push: 483 484 \b 485 none: No dependencies, just the element itself 486 all: All dependencies 487 """ 488 with app.initialized(session_name="Push"): 489 app.stream.push(elements, selection=deps, remote=remote) 490 491 492################################################################## 493# Show Command # 494################################################################## 495@cli.command(short_help="Show elements in the pipeline") 496@click.option('--except', 'except_', multiple=True, 497 type=click.Path(readable=False), 498 help="Except certain dependencies") 499@click.option('--deps', '-d', default='all', 500 type=click.Choice(['none', 'plan', 'run', 'build', 'all']), 501 help='The dependencies to show (default: all)') 502@click.option('--order', default="stage", 503 type=click.Choice(['stage', 'alpha']), 504 help='Staging or alphabetic ordering of dependencies') 505@click.option('--format', '-f', 'format_', metavar='FORMAT', default=None, 506 type=click.STRING, 507 help='Format string for each element') 508@click.argument('elements', nargs=-1, 509 type=click.Path(readable=False)) 510@click.pass_obj 511def show(app, elements, deps, except_, order, format_): 512 """Show elements in the pipeline 513 514 By default this will show all of the dependencies of the 515 specified target element. 516 517 Specify `--deps` to control which elements to show: 518 519 \b 520 none: No dependencies, just the element itself 521 plan: Dependencies required for a build plan 522 run: Runtime dependencies, including the element itself 523 build: Build time dependencies, excluding the element itself 524 all: All dependencies 525 526 \b 527 FORMAT 528 ~~~~~~ 529 The --format option controls what should be printed for each element, 530 the following symbols can be used in the format string: 531 532 \b 533 %{name} The element name 534 %{key} The abbreviated cache key (if all sources are consistent) 535 %{full-key} The full cache key (if all sources are consistent) 536 %{state} cached, buildable, waiting or inconsistent 537 %{config} The element configuration 538 %{vars} Variable configuration 539 %{env} Environment settings 540 %{public} Public domain data 541 %{workspaced} If the element is workspaced 542 %{workspace-dirs} A list of workspace directories 543 544 The value of the %{symbol} without the leading '%' character is understood 545 as a pythonic formatting string, so python formatting features apply, 546 examle: 547 548 \b 549 bst show target.bst --format \\ 550 'Name: %{name: ^20} Key: %{key: ^8} State: %{state}' 551 552 If you want to use a newline in a format string in bash, use the '$' modifier: 553 554 \b 555 bst show target.bst --format \\ 556 $'---------- %{name} ----------\\n%{vars}' 557 """ 558 with app.initialized(): 559 dependencies = app.stream.load_selection(elements, 560 selection=deps, 561 except_targets=except_) 562 563 if order == "alpha": 564 dependencies = sorted(dependencies) 565 566 if not format_: 567 format_ = app.context.log_element_format 568 569 report = app.logger.show_pipeline(dependencies, format_) 570 click.echo(report, color=app.colors) 571 572 573################################################################## 574# Shell Command # 575################################################################## 576@cli.command(short_help="Shell into an element's sandbox environment") 577@click.option('--build', '-b', 'build_', is_flag=True, default=False, 578 help='Stage dependencies and sources to build') 579@click.option('--sysroot', '-s', default=None, 580 type=click.Path(exists=True, file_okay=False, readable=True), 581 help="An existing sysroot") 582@click.option('--mount', type=click.Tuple([click.Path(exists=True), str]), multiple=True, 583 metavar='HOSTPATH PATH', 584 help="Mount a file or directory into the sandbox") 585@click.option('--isolate', is_flag=True, default=False, 586 help='Create an isolated build sandbox') 587@click.argument('element', 588 type=click.Path(readable=False)) 589@click.argument('command', type=click.STRING, nargs=-1) 590@click.pass_obj 591def shell(app, element, sysroot, mount, isolate, build_, command): 592 """Run a command in the target element's sandbox environment 593 594 This will stage a temporary sysroot for running the target 595 element, assuming it has already been built and all required 596 artifacts are in the local cache. 597 598 Use the --build option to create a temporary sysroot for 599 building the element instead. 600 601 Use the --sysroot option with an existing failed build 602 directory or with a checkout of the given target, in order 603 to use a specific sysroot. 604 605 If no COMMAND is specified, the default is to attempt 606 to run an interactive shell. 607 """ 608 from ..element import Scope 609 from .._project import HostMount 610 from .._pipeline import PipelineSelection 611 612 if build_: 613 scope = Scope.BUILD 614 else: 615 scope = Scope.RUN 616 617 with app.initialized(): 618 dependencies = app.stream.load_selection((element,), selection=PipelineSelection.NONE) 619 element = dependencies[0] 620 prompt = app.shell_prompt(element) 621 mounts = [ 622 HostMount(path, host_path) 623 for host_path, path in mount 624 ] 625 try: 626 exitcode = app.stream.shell(element, scope, prompt, 627 directory=sysroot, 628 mounts=mounts, 629 isolate=isolate, 630 command=command) 631 except BstError as e: 632 raise AppError("Error launching shell: {}".format(e), detail=e.detail) from e 633 634 # If there were no errors, we return the shell's exit code here. 635 sys.exit(exitcode) 636 637 638################################################################## 639# Checkout Command # 640################################################################## 641@cli.command(short_help="Checkout a built artifact") 642@click.option('--force', '-f', default=False, is_flag=True, 643 help="Allow files to be overwritten") 644@click.option('--deps', '-d', default='run', 645 type=click.Choice(['run', 'none']), 646 help='The dependencies to checkout (default: run)') 647@click.option('--integrate/--no-integrate', default=True, is_flag=True, 648 help="Whether to run integration commands") 649@click.option('--hardlinks', default=False, is_flag=True, 650 help="Checkout hardlinks instead of copies (handle with care)") 651@click.option('--tar', default=False, is_flag=True, 652 help="Create a tarball from the artifact contents instead " 653 "of a file tree. If LOCATION is '-', the tarball " 654 "will be dumped to the standard output.") 655@click.argument('element', 656 type=click.Path(readable=False)) 657@click.argument('location', type=click.Path()) 658@click.pass_obj 659def checkout(app, element, location, force, deps, integrate, hardlinks, tar): 660 """Checkout a built artifact to the specified location 661 """ 662 663 if hardlinks and tar: 664 click.echo("ERROR: options --hardlinks and --tar conflict", err=True) 665 sys.exit(-1) 666 667 with app.initialized(): 668 app.stream.checkout(element, 669 location=location, 670 force=force, 671 deps=deps, 672 integrate=integrate, 673 hardlinks=hardlinks, 674 tar=tar) 675 676 677################################################################## 678# Workspace Command # 679################################################################## 680@cli.group(short_help="Manipulate developer workspaces") 681def workspace(): 682 """Manipulate developer workspaces""" 683 pass 684 685 686################################################################## 687# Workspace Open Command # 688################################################################## 689@workspace.command(name='open', short_help="Open a new workspace") 690@click.option('--no-checkout', default=False, is_flag=True, 691 help="Do not checkout the source, only link to the given directory") 692@click.option('--force', '-f', default=False, is_flag=True, 693 help="Overwrite files existing in checkout directory") 694@click.option('--track', 'track_', default=False, is_flag=True, 695 help="Track and fetch new source references before checking out the workspace") 696@click.argument('element', 697 type=click.Path(readable=False)) 698@click.argument('directory', type=click.Path(file_okay=False)) 699@click.pass_obj 700def workspace_open(app, no_checkout, force, track_, element, directory): 701 """Open a workspace for manual source modification""" 702 703 if os.path.exists(directory): 704 705 if not os.path.isdir(directory): 706 click.echo("Checkout directory is not a directory: {}".format(directory), err=True) 707 sys.exit(-1) 708 709 if not (no_checkout or force) and os.listdir(directory): 710 click.echo("Checkout directory is not empty: {}".format(directory), err=True) 711 sys.exit(-1) 712 713 with app.initialized(): 714 app.stream.workspace_open(element, directory, 715 no_checkout=no_checkout, 716 track_first=track_, 717 force=force) 718 719 720################################################################## 721# Workspace Close Command # 722################################################################## 723@workspace.command(name='close', short_help="Close workspaces") 724@click.option('--remove-dir', default=False, is_flag=True, 725 help="Remove the path that contains the closed workspace") 726@click.option('--all', '-a', 'all_', default=False, is_flag=True, 727 help="Close all open workspaces") 728@click.argument('elements', nargs=-1, 729 type=click.Path(readable=False)) 730@click.pass_obj 731def workspace_close(app, remove_dir, all_, elements): 732 """Close a workspace""" 733 734 if not (all_ or elements): 735 click.echo('ERROR: no elements specified', err=True) 736 sys.exit(-1) 737 738 with app.initialized(): 739 740 # Early exit if we specified `all` and there are no workspaces 741 if all_ and not app.stream.workspace_exists(): 742 click.echo('No open workspaces to close', err=True) 743 sys.exit(0) 744 745 if all_: 746 elements = [element_name for element_name, _ in app.context.get_workspaces().list()] 747 748 elements = app.stream.redirect_element_names(elements) 749 750 # Check that the workspaces in question exist 751 nonexisting = [] 752 for element_name in elements: 753 if not app.stream.workspace_exists(element_name): 754 nonexisting.append(element_name) 755 if nonexisting: 756 raise AppError("Workspace does not exist", detail="\n".join(nonexisting)) 757 758 if app.interactive and remove_dir: 759 if not click.confirm('This will remove all your changes, are you sure?'): 760 click.echo('Aborting', err=True) 761 sys.exit(-1) 762 763 for element_name in elements: 764 app.stream.workspace_close(element_name, remove_dir=remove_dir) 765 766 767################################################################## 768# Workspace Reset Command # 769################################################################## 770@workspace.command(name='reset', short_help="Reset a workspace to its original state") 771@click.option('--soft', default=False, is_flag=True, 772 help="Reset workspace state without affecting its contents") 773@click.option('--track', 'track_', default=False, is_flag=True, 774 help="Track and fetch the latest source before resetting") 775@click.option('--all', '-a', 'all_', default=False, is_flag=True, 776 help="Reset all open workspaces") 777@click.argument('elements', nargs=-1, 778 type=click.Path(readable=False)) 779@click.pass_obj 780def workspace_reset(app, soft, track_, all_, elements): 781 """Reset a workspace to its original state""" 782 783 # Check that the workspaces in question exist 784 with app.initialized(): 785 786 if not (all_ or elements): 787 raise AppError('No elements specified to reset') 788 789 if all_ and not app.stream.workspace_exists(): 790 raise AppError("No open workspaces to reset") 791 792 if app.interactive and not soft: 793 if not click.confirm('This will remove all your changes, are you sure?'): 794 click.echo('Aborting', err=True) 795 sys.exit(-1) 796 797 if all_: 798 elements = tuple(element_name for element_name, _ in app.context.get_workspaces().list()) 799 800 app.stream.workspace_reset(elements, soft=soft, track_first=track_) 801 802 803################################################################## 804# Workspace List Command # 805################################################################## 806@workspace.command(name='list', short_help="List open workspaces") 807@click.pass_obj 808def workspace_list(app): 809 """List open workspaces""" 810 811 with app.initialized(): 812 app.stream.workspace_list() 813 814 815################################################################## 816# Source Bundle Command # 817################################################################## 818@cli.command(name="source-bundle", short_help="Produce a build bundle to be manually executed") 819@click.option('--except', 'except_', multiple=True, 820 type=click.Path(readable=False), 821 help="Elements to except from the tarball") 822@click.option('--compression', default='gz', 823 type=click.Choice(['none', 'gz', 'bz2', 'xz']), 824 help="Compress the tar file using the given algorithm.") 825@click.option('--track', 'track_', default=False, is_flag=True, 826 help="Track new source references before bundling") 827@click.option('--force', '-f', default=False, is_flag=True, 828 help="Overwrite an existing tarball") 829@click.option('--directory', default=os.getcwd(), 830 help="The directory to write the tarball to") 831@click.argument('element', 832 type=click.Path(readable=False)) 833@click.pass_obj 834def source_bundle(app, element, force, directory, 835 track_, compression, except_): 836 """Produce a source bundle to be manually executed 837 """ 838 with app.initialized(): 839 app.stream.source_bundle(element, directory, 840 track_first=track_, 841 force=force, 842 compression=compression, 843 except_targets=except_) 844