| import os |
| import sys |
| from functools import partial |
| |
| import shutil |
| import click |
| from .. import _yaml |
| from .._exceptions import BstError, LoadError, AppError, RemoteError |
| from .complete import main_bashcomplete, complete_path, CompleteUnhandled |
| from ..types import _CacheBuildTrees, _SchedulerErrorAction, _PipelineSelection, _HostMount, _Scope |
| from .._remotespec import RemoteSpec, RemoteSpecPurpose |
| from ..utils import UtilError |
| |
| |
| ################################################################## |
| # Helper classes and methods for Click # |
| ################################################################## |
| |
| |
| class FastEnumType(click.Choice): |
| def __init__(self, enum, options=None): |
| self._enum = enum |
| |
| if options is None: |
| options = enum.values() |
| else: |
| options = [option.value for option in options] |
| |
| super().__init__(options) |
| |
| def convert(self, value, param, ctx): |
| # This allows specifying default values as instances of the |
| # enum |
| if isinstance(value, self._enum): |
| value = value.value |
| |
| return self._enum(super().convert(value, param, ctx)) |
| |
| |
| class RemoteSpecType(click.ParamType): |
| name = "remote" |
| |
| def __init__(self, purpose=RemoteSpecPurpose.ALL): |
| self.purpose = purpose |
| |
| def convert(self, value, param, ctx): |
| spec = None |
| try: |
| spec = RemoteSpec.new_from_string(value, self.purpose) |
| except RemoteError as e: |
| self.fail("Failed to interpret remote: {}".format(e)) |
| |
| return spec |
| |
| def __repr__(self): |
| return "REMOTE" |
| |
| |
| ################################################################## |
| # 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) |
| if not command_ctx.command.get_command(command_ctx, subcommand).hidden |
| ] |
| |
| return [] |
| |
| |
| # Special completion for completing the bst elements in a project dir |
| def complete_target(args, incomplete): |
| """ |
| :param args: full list of args typed before the incomplete arg |
| :param incomplete: the incomplete text to autocomplete |
| :return: all the possible user-specified completions for the param |
| """ |
| |
| from .. import utils |
| |
| project_conf = "project.conf" |
| |
| # First resolve the directory, in case there is an |
| # active --directory/-C option |
| # |
| base_directory = "." |
| idx = -1 |
| try: |
| idx = args.index("-C") |
| except ValueError: |
| try: |
| idx = args.index("--directory") |
| except ValueError: |
| pass |
| |
| 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, _ = utils._search_upward_for_files(base_directory, [project_conf]) |
| |
| if base_directory is None: |
| # No project_conf was found in base_directory or its parents, no need |
| # to try loading any project conf and avoid os.path NoneType TypeError. |
| return [] |
| else: |
| project_file = os.path.join(base_directory, project_conf) |
| try: |
| project = _yaml.load(project_file, shortname=project_conf) |
| except LoadError: |
| # If there is no project conf in context, just dont |
| # even bother trying to complete anything. |
| return [] |
| |
| # The project is not required to have an element-path |
| element_directory = project.get_str("element-path", default="") |
| |
| # If a project was loaded, use its element-path to |
| # adjust our completion's base directory |
| if element_directory: |
| base_directory = os.path.join(base_directory, element_directory) |
| |
| complete_list = [] |
| for p in complete_path("File", incomplete, base_directory=base_directory): |
| if p.endswith(".bst ") or p.endswith("/"): |
| complete_list.append(p) |
| return complete_list |
| |
| |
| def complete_artifact(orig_args, args, incomplete): |
| from .._context import Context |
| |
| with Context(use_casd=False) as ctx: |
| |
| config = None |
| if orig_args: |
| for i, arg in enumerate(orig_args): |
| if arg in ("-c", "--config"): |
| try: |
| config = orig_args[i + 1] |
| except IndexError: |
| pass |
| if args: |
| for i, arg in enumerate(args): |
| if arg in ("-c", "--config"): |
| try: |
| config = args[i + 1] |
| except IndexError: |
| pass |
| ctx.load(config) |
| |
| # element targets are valid artifact names |
| complete_list = complete_target(args, incomplete) |
| complete_list.extend(ref for ref in ctx.artifactcache.list_artifacts() if ref.startswith(incomplete)) |
| |
| return complete_list |
| |
| |
| def override_completions(orig_args, cmd, cmd_param, args, incomplete): |
| """ |
| :param orig_args: original, non-completion args |
| :param cmd_param: command definition |
| :param args: full list of args typed before the incomplete arg |
| :param incomplete: the incomplete text to autocomplete |
| :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. |
| if isinstance(cmd_param.type, click.Path): |
| if cmd_param.name == "elements" or cmd_param.name == "element" or cmd_param.name == "except_": |
| return complete_target(args, incomplete) |
| if cmd_param.name == "artifacts" or cmd_param.name == "target": |
| return complete_artifact(orig_args, args, incomplete) |
| |
| raise CompleteUnhandled() |
| |
| |
| def validate_output_streams(): |
| if sys.platform == "win32": |
| # Windows does not support 'fcntl', the module is unavailable there as |
| # of Python 3.7, therefore early-out here. |
| return |
| |
| import fcntl |
| |
| for stream in (sys.stdout, sys.stderr): |
| fileno = stream.fileno() |
| flags = fcntl.fcntl(fileno, fcntl.F_GETFL) |
| if flags & os.O_NONBLOCK: |
| click.echo("{} is currently set to O_NONBLOCK, try opening a new shell".format(stream.name), err=True) |
| sys.exit(-1) |
| |
| |
| def override_main(self, args=None, prog_name=None, complete_var=None, standalone_mode=True, **extra): |
| |
| # Hook for the Bash completion. This only activates if the Bash |
| # completion is actually enabled, otherwise this is quite a fast |
| # noop. |
| if main_bashcomplete(self, prog_name, partial(override_completions, args)): |
| |
| # If we're running tests we cant just go calling exit() |
| # from the main process. |
| # |
| # The below is a quicker exit path for the sake |
| # of making completions respond faster. |
| if "BST_TEST_SUITE" not in os.environ: |
| sys.stdout.flush() |
| sys.stderr.flush() |
| os._exit(0) |
| |
| # Regular client return for test cases |
| return |
| |
| # Check output file descriptor at earliest opportunity, to |
| # provide a reasonable error message instead of a stack trace |
| # in the case that it is non-blocking. |
| validate_output_streams() |
| |
| original_main(self, args=args, prog_name=prog_name, complete_var=None, standalone_mode=standalone_mode, **extra) |
| |
| |
| original_main = click.BaseCommand.main |
| # Disable type checking since mypy doesn't support assigning to a method. |
| # See https://github.com/python/mypy/issues/2427. |
| click.BaseCommand.main = override_main # type: ignore |
| |
| |
| ################################################################## |
| # Main Options # |
| ################################################################## |
| def print_version(ctx, param, value): |
| if not value or ctx.resilient_parsing: |
| return |
| |
| from .. import __version__ |
| |
| click.echo(__version__) |
| ctx.exit() |
| |
| |
| @click.group(context_settings=dict(help_option_names=["-h", "--help"])) |
| @click.option("--version", is_flag=True, callback=print_version, expose_value=False, is_eager=True) |
| @click.option( |
| "--config", "-c", type=click.Path(exists=True, dir_okay=False, readable=True), help="Configuration file to use" |
| ) |
| @click.option( |
| "--directory", |
| "-C", |
| default=None, # Set to os.getcwd() later. |
| type=click.Path(file_okay=False, readable=True), |
| help="Project directory (default: current directory)", |
| ) |
| @click.option( |
| "--on-error", |
| default=None, |
| type=FastEnumType(_SchedulerErrorAction), |
| help="What to do when an error is encountered", |
| ) |
| @click.option("--fetchers", type=click.INT, default=None, help="Maximum simultaneous download tasks") |
| @click.option("--builders", type=click.INT, default=None, help="Maximum simultaneous build tasks") |
| @click.option("--pushers", type=click.INT, default=None, help="Maximum simultaneous upload tasks") |
| @click.option( |
| "--max-jobs", type=click.INT, default=None, help="Number of parallel jobs allowed for a given build task" |
| ) |
| @click.option("--network-retries", type=click.INT, default=None, help="Maximum retries for network tasks") |
| @click.option( |
| "--no-interactive", is_flag=True, help="Force non interactive mode, otherwise this is automatically decided" |
| ) |
| @click.option("--verbose/--no-verbose", default=None, help="Be extra verbose") |
| @click.option("--debug/--no-debug", default=None, help="Print debugging output") |
| @click.option("--error-lines", type=click.INT, default=None, help="Maximum number of lines to show from a task log") |
| @click.option( |
| "--message-lines", type=click.INT, default=None, help="Maximum number of lines to show in a detailed message" |
| ) |
| @click.option( |
| "--log-file", |
| type=click.File(mode="w", encoding="UTF-8"), |
| help="A file to store the main log (allows storing the main log while in interactive mode)", |
| ) |
| @click.option("--colors/--no-colors", default=None, help="Force enable/disable ANSI color codes in output") |
| @click.option( |
| "--strict/--no-strict", |
| default=None, |
| is_flag=True, |
| help="Elements must be rebuilt when their dependencies have changed", |
| ) |
| @click.option( |
| "--option", |
| "-o", |
| type=click.Tuple([str, str]), |
| multiple=True, |
| metavar="OPTION VALUE", |
| help="Specify a project option", |
| ) |
| @click.option("--default-mirror", default=None, help="The mirror to fetch from first, before attempting other mirrors") |
| @click.option( |
| "--pull-buildtrees", |
| is_flag=True, |
| default=None, |
| help="Include an element's build tree when pulling remote element artifacts", |
| ) |
| @click.option( |
| "--cache-buildtrees", |
| default=None, |
| type=FastEnumType(_CacheBuildTrees), |
| help="Cache artifact build tree content on creation", |
| ) |
| @click.pass_context |
| def cli(context, **kwargs): |
| """Build and manipulate BuildStream projects |
| |
| Most of the main options override options in the |
| user preferences configuration file. |
| """ |
| |
| from .app import App |
| |
| # Create the App, giving it the main arguments |
| context.obj = App.create(dict(kwargs)) |
| context.call_on_close(context.obj.cleanup) |
| |
| # Configure colors |
| context.color = context.obj.colors |
| |
| |
| ################################################################## |
| # 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") |
| @click.option("--project-name", type=click.STRING, help="The project name to use") |
| @click.option( |
| "--min-version", type=click.STRING, default="2.0", show_default=True, help="The required format version", |
| ) |
| @click.option( |
| "--element-path", |
| type=click.Path(), |
| default="elements", |
| show_default=True, |
| help="The subdirectory to store elements in", |
| ) |
| @click.option("--force", "-f", is_flag=True, help="Allow overwriting an existing project.conf") |
| @click.argument("target-directory", nargs=1, required=False, type=click.Path(file_okay=False, writable=True)) |
| @click.pass_obj |
| def init(app, project_name, min_version, element_path, force, target_directory): |
| """Initialize a new BuildStream project |
| |
| Creates a new BuildStream project.conf in the project |
| directory. |
| |
| Unless `--project-name` is specified, this will be an |
| interactive session. |
| """ |
| app.init_project(project_name, min_version, element_path, force, target_directory) |
| |
| |
| ################################################################## |
| # Build Command # |
| ################################################################## |
| @cli.command(short_help="Build elements in a pipeline") |
| @click.option( |
| "--deps", |
| "-d", |
| default=None, |
| type=FastEnumType( |
| _PipelineSelection, [_PipelineSelection.BUILD, _PipelineSelection.PLAN, _PipelineSelection.ALL], |
| ), |
| help="The dependencies to build", |
| ) |
| @click.option( |
| "--artifact-remote", |
| "artifact_remotes", |
| type=RemoteSpecType(), |
| multiple=True, |
| help="A remote for uploading and downloading artifacts", |
| ) |
| @click.option( |
| "--source-remote", |
| "source_remotes", |
| type=RemoteSpecType(), |
| multiple=True, |
| help="A remote for uploading and downloading cached sources", |
| ) |
| @click.option( |
| "--ignore-project-artifact-remotes", |
| is_flag=True, |
| help="Ignore remote artifact cache servers recommended by projects", |
| ) |
| @click.option( |
| "--ignore-project-source-remotes", is_flag=True, help="Ignore remote source cache servers recommended by projects" |
| ) |
| @click.argument("elements", nargs=-1, type=click.Path(readable=False)) |
| @click.pass_obj |
| def build( |
| app, |
| elements, |
| deps, |
| artifact_remotes, |
| source_remotes, |
| ignore_project_artifact_remotes, |
| ignore_project_source_remotes, |
| ): |
| """Build elements in a pipeline |
| |
| Specifying no elements will result in building the default targets |
| of the project. If no default targets are configured, all project |
| elements will be built. |
| |
| When this command is executed from a workspace directory, the default |
| is to build the workspace element. |
| |
| Specify `--deps` to control which dependencies to build: |
| |
| \b |
| plan: Only dependencies required for the build plan |
| build: Build time dependencies, excluding the element itself |
| all: All dependencies |
| """ |
| with app.initialized(session_name="Build"): |
| ignore_junction_targets = False |
| |
| if deps is None: |
| deps = app.context.build_dependencies |
| |
| if not elements: |
| elements = app.project.get_default_targets() |
| # Junction elements cannot be built, exclude them from default targets |
| ignore_junction_targets = True |
| |
| app.stream.build( |
| elements, |
| selection=deps, |
| ignore_junction_targets=ignore_junction_targets, |
| artifact_remotes=artifact_remotes, |
| source_remotes=source_remotes, |
| ignore_project_artifact_remotes=ignore_project_artifact_remotes, |
| ignore_project_source_remotes=ignore_project_source_remotes, |
| ) |
| |
| |
| ################################################################## |
| # Show Command # |
| ################################################################## |
| @cli.command(short_help="Show elements in the pipeline") |
| @click.option( |
| "--except", "except_", multiple=True, type=click.Path(readable=False), help="Except certain dependencies" |
| ) |
| @click.option( |
| "--deps", |
| "-d", |
| default=_PipelineSelection.ALL, |
| show_default=True, |
| type=FastEnumType( |
| _PipelineSelection, |
| [ |
| _PipelineSelection.NONE, |
| _PipelineSelection.PLAN, |
| _PipelineSelection.RUN, |
| _PipelineSelection.BUILD, |
| _PipelineSelection.ALL, |
| ], |
| ), |
| help="The dependencies to show", |
| ) |
| @click.option( |
| "--order", |
| default="stage", |
| show_default=True, |
| type=click.Choice(["stage", "alpha"]), |
| help="Staging or alphabetic ordering of dependencies", |
| ) |
| @click.option( |
| "--format", |
| "-f", |
| "format_", |
| metavar="FORMAT", |
| default=None, |
| type=click.STRING, |
| help="Format string for each element", |
| ) |
| @click.argument("elements", nargs=-1, type=click.Path(readable=False)) |
| @click.pass_obj |
| def show(app, elements, deps, except_, order, format_): |
| """Show elements in the pipeline |
| |
| Specifying no elements will result in showing the default targets |
| of the project. If no default targets are configured, all project |
| elements will be shown. |
| |
| When this command is executed from a workspace directory, the default |
| is to show the workspace element. |
| |
| By default this will show all of the dependencies of the |
| specified target element. |
| |
| Specify ``--deps`` to control which elements to show: |
| |
| \b |
| none: No dependencies, just the element itself |
| plan: Dependencies required for a build plan |
| run: Runtime dependencies, including the element itself |
| build: Build time dependencies, excluding the element itself |
| all: All dependencies |
| |
| **FORMAT** |
| |
| The ``--format`` option controls what should be printed for each element, |
| the following symbols can be used in the format string: |
| |
| \b |
| %{name} The element name |
| %{key} The abbreviated cache key (if all sources are consistent) |
| %{full-key} The full cache key (if all sources are consistent) |
| %{state} cached, buildable, waiting, inconsistent or junction |
| %{config} The element configuration |
| %{vars} Variable configuration |
| %{env} Environment settings |
| %{public} Public domain data |
| %{workspaced} If the element is workspaced |
| %{workspace-dirs} A list of workspace directories |
| %{deps} A list of all dependencies |
| %{build-deps} A list of build dependencies |
| %{runtime-deps} A list of runtime dependencies |
| |
| The value of the %{symbol} without the leading '%' character is understood |
| as a pythonic formatting string, so python formatting features apply, |
| example: |
| |
| \b |
| bst show target.bst --format \\ |
| 'Name: %{name: ^20} Key: %{key: ^8} State: %{state}' |
| |
| If you want to use a newline in a format string in bash, use the '$' modifier: |
| |
| \b |
| bst show target.bst --format \\ |
| $'---------- %{name} ----------\\n%{vars}' |
| """ |
| with app.initialized(): |
| if not elements: |
| elements = app.project.get_default_targets() |
| |
| dependencies = app.stream.load_selection(elements, selection=deps, except_targets=except_) |
| |
| app.stream.query_cache(dependencies) |
| |
| if order == "alpha": |
| dependencies = sorted(dependencies) |
| |
| if not format_: |
| format_ = app.context.log_element_format |
| |
| report = app.logger.show_pipeline(dependencies, format_) |
| click.echo(report) |
| |
| |
| ################################################################## |
| # Shell Command # |
| ################################################################## |
| @cli.command(short_help="Shell into an element's sandbox environment") |
| @click.option("--build", "-b", "build_", is_flag=True, help="Stage dependencies and sources to build") |
| @click.option( |
| "--mount", |
| type=click.Tuple([click.Path(exists=True), str]), |
| multiple=True, |
| metavar="HOSTPATH PATH", |
| help="Mount a file or directory into the sandbox", |
| ) |
| @click.option("--isolate", is_flag=True, help="Create an isolated build sandbox") |
| @click.option( |
| "--use-buildtree", |
| "-t", |
| "cli_buildtree", |
| is_flag=True, |
| help=( |
| "Stage a buildtree. Will fail if a buildtree is not available." |
| " --pull and pull-buildtrees configuration is needed " |
| "if trying to query for remotely cached buildtrees." |
| ), |
| ) |
| @click.option("--pull", "pull_", is_flag=True, help="Attempt to pull missing or incomplete artifacts") |
| @click.option( |
| "--artifact-remote", |
| "artifact_remotes", |
| type=RemoteSpecType(), |
| multiple=True, |
| help="A remote for uploading and downloading artifacts", |
| ) |
| @click.option( |
| "--source-remote", |
| "source_remotes", |
| type=RemoteSpecType(), |
| multiple=True, |
| help="A remote for uploading and downloading cached sources", |
| ) |
| @click.option( |
| "--ignore-project-artifact-remotes", |
| is_flag=True, |
| help="Ignore remote artifact cache servers recommended by projects", |
| ) |
| @click.option( |
| "--ignore-project-source-remotes", is_flag=True, help="Ignore remote source cache servers recommended by projects" |
| ) |
| @click.argument("element", required=False, type=click.Path(readable=False)) |
| @click.argument("command", type=click.STRING, nargs=-1) |
| @click.pass_obj |
| def shell( |
| app, |
| element, |
| command, |
| mount, |
| isolate, |
| build_, |
| cli_buildtree, |
| pull_, |
| artifact_remotes, |
| source_remotes, |
| ignore_project_artifact_remotes, |
| ignore_project_source_remotes, |
| ): |
| """Run a command in the target element's sandbox environment |
| |
| When this command is executed from a workspace directory, the default |
| is to shell into the workspace element. |
| |
| This will stage a temporary sysroot for running the target |
| element, assuming it has already been built and all required |
| artifacts are in the local cache. |
| |
| Use '--' to separate a command from the options to bst, |
| otherwise bst may respond to them instead. e.g. |
| |
| \b |
| bst shell example.bst -- df -h |
| |
| Use the --build option to create a temporary sysroot for |
| building the element instead. |
| |
| If no COMMAND is specified, the default is to attempt |
| to run an interactive shell. |
| """ |
| |
| # Buildtree can only be used with build shells |
| if cli_buildtree: |
| build_ = True |
| |
| scope = _Scope.BUILD if build_ else _Scope.RUN |
| |
| with app.initialized(): |
| if not element: |
| element = app.project.get_default_target() |
| if not element: |
| raise AppError('Missing argument "ELEMENT".') |
| |
| mounts = [_HostMount(path, host_path) for host_path, path in mount] |
| |
| try: |
| exitcode = app.stream.shell( |
| element, |
| scope, |
| app.shell_prompt, |
| mounts=mounts, |
| isolate=isolate, |
| command=command, |
| usebuildtree=cli_buildtree, |
| pull_=pull_, |
| artifact_remotes=artifact_remotes, |
| source_remotes=source_remotes, |
| ignore_project_artifact_remotes=ignore_project_artifact_remotes, |
| ignore_project_source_remotes=ignore_project_source_remotes, |
| ) |
| except BstError as e: |
| raise AppError("Error launching shell: {}".format(e), detail=e.detail, reason=e.reason) from e |
| |
| # If there were no errors, we return the shell's exit code here. |
| sys.exit(exitcode) |
| |
| |
| ################################################################## |
| # Source Command # |
| ################################################################## |
| @cli.group(short_help="Manipulate sources for an element") |
| def source(): |
| """Manipulate sources for an element""" |
| |
| |
| ################################################################## |
| # Source Fetch Command # |
| ################################################################## |
| @source.command(name="fetch", short_help="Fetch sources in a pipeline") |
| @click.option( |
| "--except", |
| "except_", |
| multiple=True, |
| type=click.Path(readable=False), |
| help="Except certain dependencies from fetching", |
| ) |
| @click.option( |
| "--deps", |
| "-d", |
| default=_PipelineSelection.PLAN, |
| show_default=True, |
| type=FastEnumType( |
| _PipelineSelection, |
| [ |
| _PipelineSelection.PLAN, |
| _PipelineSelection.NONE, |
| _PipelineSelection.BUILD, |
| _PipelineSelection.RUN, |
| _PipelineSelection.ALL, |
| ], |
| ), |
| help="The dependencies to fetch", |
| ) |
| @click.option( |
| "--source-remote", |
| "source_remotes", |
| type=RemoteSpecType(RemoteSpecPurpose.PULL), |
| multiple=True, |
| help="A remote for downloading sources", |
| ) |
| @click.option( |
| "--ignore-project-source-remotes", is_flag=True, help="Ignore remote source cache servers recommended by projects" |
| ) |
| @click.argument("elements", nargs=-1, type=click.Path(readable=False)) |
| @click.pass_obj |
| def source_fetch(app, elements, deps, except_, source_remotes, ignore_project_source_remotes): |
| """Fetch sources required to build the pipeline |
| |
| Specifying no elements will result in fetching the default targets |
| of the project. If no default targets are configured, all project |
| elements will be fetched. |
| |
| When this command is executed from a workspace directory, the default |
| is to fetch the workspace element. |
| |
| By default this will only try to fetch sources which are |
| required for the build plan of the specified target element, |
| omitting sources for any elements which are already built |
| and available in the artifact cache. |
| |
| Specify `--deps` to control which sources to fetch: |
| |
| \b |
| none: No dependencies, just the element itself |
| plan: Only dependencies required for the build plan |
| run: Runtime dependencies, including the element itself |
| build: Build time dependencies, excluding the element itself |
| all: All dependencies |
| """ |
| with app.initialized(session_name="Fetch"): |
| if not elements: |
| elements = app.project.get_default_targets() |
| |
| app.stream.fetch( |
| elements, |
| selection=deps, |
| except_targets=except_, |
| source_remotes=source_remotes, |
| ignore_project_source_remotes=ignore_project_source_remotes, |
| ) |
| |
| |
| ################################################################## |
| # Source Push Command # |
| ################################################################## |
| @source.command(name="push", short_help="Push sources in a pipeline") |
| @click.option( |
| "--except", |
| "except_", |
| multiple=True, |
| type=click.Path(readable=False), |
| help="Except certain dependencies from pushing", |
| ) |
| @click.option( |
| "--deps", |
| "-d", |
| default=_PipelineSelection.NONE, |
| show_default=True, |
| type=FastEnumType( |
| _PipelineSelection, |
| [ |
| _PipelineSelection.NONE, |
| _PipelineSelection.PLAN, |
| _PipelineSelection.BUILD, |
| _PipelineSelection.RUN, |
| _PipelineSelection.ALL, |
| ], |
| ), |
| help="The dependencies to push", |
| ) |
| @click.option( |
| "--source-remote", |
| "source_remotes", |
| type=RemoteSpecType(RemoteSpecPurpose.PUSH), |
| multiple=True, |
| help="A remote for uploading sources", |
| ) |
| @click.option( |
| "--ignore-project-source-remotes", is_flag=True, help="Ignore remote source cache servers recommended by projects" |
| ) |
| @click.argument("elements", nargs=-1, type=click.Path(readable=False)) |
| @click.pass_obj |
| def source_push(app, elements, deps, except_, source_remotes, ignore_project_source_remotes): |
| """Push sources required to build the pipeline |
| |
| Specifying no elements will result in pushing the sources of the default |
| targets of the project. If no default targets are configured, sources of |
| all project elements will be pushed. |
| |
| When this command is executed from a workspace directory, the default |
| is to push the sources of the workspace element. |
| |
| Specify `--deps` to control which sources to fetch: |
| |
| \b |
| none: No dependencies, just the element itself |
| plan: Only dependencies required for the build plan |
| run: Runtime dependencies, including the element itself |
| build: Build time dependencies, excluding the element itself |
| all: All dependencies |
| """ |
| with app.initialized(session_name="Push"): |
| if not elements: |
| elements = app.project.get_default_targets() |
| |
| app.stream.source_push( |
| elements, |
| selection=deps, |
| except_targets=except_, |
| source_remotes=source_remotes, |
| ignore_project_source_remotes=ignore_project_source_remotes, |
| ) |
| |
| |
| ################################################################## |
| # Source Track Command # |
| ################################################################## |
| @source.command(name="track", short_help="Track new source references") |
| @click.option( |
| "--except", |
| "except_", |
| multiple=True, |
| type=click.Path(readable=False), |
| help="Except certain dependencies from tracking", |
| ) |
| @click.option( |
| "--deps", |
| "-d", |
| default=_PipelineSelection.NONE, |
| show_default=True, |
| type=FastEnumType( |
| _PipelineSelection, |
| [_PipelineSelection.BUILD, _PipelineSelection.RUN, _PipelineSelection.ALL, _PipelineSelection.NONE], |
| ), |
| help="The dependencies to track", |
| ) |
| @click.option("--cross-junctions", "-J", is_flag=True, help="Allow crossing junction boundaries") |
| @click.argument("elements", nargs=-1, type=click.Path(readable=False)) |
| @click.pass_obj |
| def source_track(app, elements, deps, except_, cross_junctions): |
| """Consults the specified tracking branches for new versions available |
| to build and updates the project with any newly available references. |
| |
| Specifying no elements will result in tracking the default targets |
| of the project. If no default targets are configured, all project |
| elements will be tracked. |
| |
| When this command is executed from a workspace directory, the default |
| is to track the workspace element. |
| |
| If no default is declared, all elements in the project will be tracked |
| |
| By default this will track just the specified element, but you can also |
| update a whole tree of dependencies in one go. |
| |
| Specify `--deps` to control which sources to track: |
| |
| \b |
| none: No dependencies, just the specified elements |
| all: All dependencies of all specified elements |
| """ |
| with app.initialized(session_name="Track"): |
| if not elements: |
| elements = app.project.get_default_targets() |
| |
| # Substitute 'none' for 'redirect' so that element redirections |
| # will be done |
| if deps == _PipelineSelection.NONE: |
| deps = _PipelineSelection.REDIRECT |
| app.stream.track(elements, selection=deps, except_targets=except_, cross_junctions=cross_junctions) |
| |
| |
| ################################################################## |
| # Source Checkout Command # |
| ################################################################## |
| @source.command(name="checkout", short_help="Checkout sources of an element") |
| @click.option("--force", "-f", is_flag=True, help="Allow files to be overwritten") |
| @click.option( |
| "--except", "except_", multiple=True, type=click.Path(readable=False), help="Except certain dependencies" |
| ) |
| @click.option( |
| "--deps", |
| "-d", |
| default=_PipelineSelection.NONE, |
| show_default=True, |
| type=FastEnumType( |
| _PipelineSelection, |
| [_PipelineSelection.BUILD, _PipelineSelection.NONE, _PipelineSelection.RUN, _PipelineSelection.ALL], |
| ), |
| help="The dependencies whose sources to checkout", |
| ) |
| @click.option( |
| "--tar", |
| default=None, |
| metavar="LOCATION", |
| type=click.Path(), |
| help="Create a tarball containing the sources instead " "of a file tree.", |
| ) |
| @click.option( |
| "--compression", |
| default=None, |
| type=click.Choice(["gz", "xz", "bz2"]), |
| help="The compression option of the tarball created.", |
| ) |
| @click.option("--include-build-scripts", "build_scripts", is_flag=True) |
| @click.option( |
| "--directory", |
| default="source-checkout", |
| type=click.Path(file_okay=False), |
| help="The directory to checkout the sources to", |
| ) |
| @click.option( |
| "--source-remote", |
| "source_remotes", |
| type=RemoteSpecType(RemoteSpecPurpose.PULL), |
| multiple=True, |
| help="A remote for downloading cached sources", |
| ) |
| @click.option( |
| "--ignore-project-source-remotes", is_flag=True, help="Ignore remote source cache servers recommended by projects" |
| ) |
| @click.argument("element", required=False, type=click.Path(readable=False)) |
| @click.pass_obj |
| def source_checkout( |
| app, |
| element, |
| directory, |
| force, |
| deps, |
| except_, |
| tar, |
| compression, |
| build_scripts, |
| source_remotes, |
| ignore_project_source_remotes, |
| ): |
| """Checkout sources of an element to the specified location |
| |
| When this command is executed from a workspace directory, the default |
| is to checkout the sources of the workspace element. |
| """ |
| |
| if tar and directory != "source-checkout": |
| click.echo("ERROR: options --directory and --tar conflict", err=True) |
| sys.exit(-1) |
| |
| if compression and not tar: |
| click.echo("ERROR: --compression specified without --tar", err=True) |
| sys.exit(-1) |
| |
| # Set the location depending on whether --tar/--directory were specified |
| # Note that if unset, --directory defaults to "source-checkout" |
| location = tar if tar else directory |
| |
| with app.initialized(): |
| if not element: |
| element = app.project.get_default_target() |
| if not element: |
| raise AppError('Missing argument "ELEMENT".') |
| |
| app.stream.source_checkout( |
| element, |
| location=location, |
| force=force, |
| deps=deps, |
| except_targets=except_, |
| tar=bool(tar), |
| compression=compression, |
| include_build_scripts=build_scripts, |
| source_remotes=source_remotes, |
| ignore_project_source_remotes=ignore_project_source_remotes, |
| ) |
| |
| |
| ################################################################## |
| # Workspace Command # |
| ################################################################## |
| @cli.group(short_help="Manipulate developer workspaces") |
| def workspace(): |
| """Manipulate developer workspaces""" |
| |
| |
| ################################################################## |
| # Workspace Open Command # |
| ################################################################## |
| @workspace.command(name="open", short_help="Open a new workspace") |
| @click.option("--no-checkout", is_flag=True, help="Do not checkout the source, only link to the given directory") |
| @click.option( |
| "--force", |
| "-f", |
| is_flag=True, |
| help="The workspace will be created even if the directory in which it will be created is not empty " |
| + "or if a workspace for that element already exists", |
| ) |
| @click.option( |
| "--directory", |
| type=click.Path(file_okay=False), |
| default=None, |
| help="Only for use when a single Element is given: Set the directory to use to create the workspace", |
| ) |
| @click.option( |
| "--source-remote", |
| "source_remotes", |
| type=RemoteSpecType(RemoteSpecPurpose.PULL), |
| multiple=True, |
| help="A remote for downloading cached sources", |
| ) |
| @click.option( |
| "--ignore-project-source-remotes", is_flag=True, help="Ignore remote source cache servers recommended by projects" |
| ) |
| @click.argument("elements", nargs=-1, type=click.Path(readable=False), required=True) |
| @click.pass_obj |
| def workspace_open(app, no_checkout, force, directory, source_remotes, ignore_project_source_remotes, elements): |
| """Open a workspace for manual source modification""" |
| |
| with app.initialized(): |
| app.stream.workspace_open( |
| elements, |
| no_checkout=no_checkout, |
| force=force, |
| custom_dir=directory, |
| source_remotes=source_remotes, |
| ignore_project_source_remotes=ignore_project_source_remotes, |
| ) |
| |
| |
| ################################################################## |
| # Workspace Close Command # |
| ################################################################## |
| @workspace.command(name="close", short_help="Close workspaces") |
| @click.option("--remove-dir", is_flag=True, help="Remove the path that contains the closed workspace") |
| @click.option("--all", "-a", "all_", is_flag=True, help="Close all open workspaces") |
| @click.argument("elements", nargs=-1, type=click.Path(readable=False)) |
| @click.pass_obj |
| def workspace_close(app, remove_dir, all_, elements): |
| """Close a workspace""" |
| |
| with app.initialized(): |
| if not (all_ or elements): |
| # NOTE: I may need to revisit this when implementing multiple projects |
| # opening one workspace. |
| element = app.project.get_default_target() |
| if element: |
| elements = (element,) |
| else: |
| raise AppError("No elements specified") |
| |
| # Early exit if we specified `all` and there are no workspaces |
| if all_ and not app.stream.workspace_exists(): |
| click.echo("No open workspaces to close", err=True) |
| sys.exit(0) |
| |
| if all_: |
| elements = [element_name for element_name, _ in app.context.get_workspaces().list()] |
| |
| elements = app.stream.redirect_element_names(elements) |
| |
| # Check that the workspaces in question exist, and that it's safe to |
| # remove them. |
| nonexisting = [] |
| for element_name in elements: |
| if not app.stream.workspace_exists(element_name): |
| nonexisting.append(element_name) |
| if nonexisting: |
| raise AppError("Workspace does not exist", detail="\n".join(nonexisting)) |
| |
| for element_name in elements: |
| app.stream.workspace_close(element_name, remove_dir=remove_dir) |
| |
| |
| ################################################################## |
| # Workspace Reset Command # |
| ################################################################## |
| @workspace.command(name="reset", short_help="Reset a workspace to its original state") |
| @click.option( |
| "--soft", |
| is_flag=True, |
| help="Mark workspace to re-execute configuration steps (if any) on next build. Does not alter workspace contents.", |
| ) |
| @click.option("--all", "-a", "all_", is_flag=True, help="Reset all open workspaces") |
| @click.argument("elements", nargs=-1, type=click.Path(readable=False)) |
| @click.pass_obj |
| def workspace_reset(app, soft, all_, elements): |
| """Reset a workspace to its original state""" |
| |
| # Check that the workspaces in question exist |
| with app.initialized(): |
| |
| if not (all_ or elements): |
| element = app.project.get_default_target() |
| 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") |
| |
| if all_: |
| elements = tuple(element_name for element_name, _ in app.context.get_workspaces().list()) |
| |
| app.stream.workspace_reset(elements, soft=soft) |
| |
| |
| ################################################################## |
| # Workspace List Command # |
| ################################################################## |
| @workspace.command(name="list", short_help="List open workspaces") |
| @click.pass_obj |
| def workspace_list(app): |
| """List open workspaces""" |
| |
| with app.initialized(): |
| app.stream.workspace_list() |
| |
| |
| ############################################################# |
| # Artifact Commands # |
| ############################################################# |
| @cli.group(short_help="Manipulate cached artifacts.") |
| def artifact(): |
| """Manipulate cached artifacts |
| |
| Some subcommands take artifact references as arguments. Artifacts |
| can be specified in two ways: |
| |
| \b |
| - artifact refs: triples of the form <project name>/<element name>/<cache key> |
| - element paths |
| |
| When elements are given, the artifact corresponding to the element is used. |
| |
| The commands also support shell-style wildcard expansion: `?` matches a |
| single character, and `*` matches zero or more. The patterns are matched |
| against artifact refs by default. If the pattern ends with `.bst` then |
| it matches element paths instead. Some example arguments are: |
| |
| \b |
| - `myproject/hello/8276376b077eda104c812e6ec2f488c7c9eea211ce572c83d734c10bf241209f` |
| - `myproject/he*/827637*` |
| - `*.bst` (all elements) |
| - `myproject/*` (all artifacts from myproject) |
| """ |
| # Note that the backticks in the above docstring are important for the |
| # generated docs. When sphinx is generating rst output from the help output |
| # of this command, the asterisks will be interpreted as emphasis tokens if |
| # they are not somehow escaped. |
| |
| |
| ############################################################# |
| # Artifact show Command # |
| ############################################################# |
| @artifact.command(name="show", short_help="Show the cached state of artifacts") |
| @click.option( |
| "--deps", |
| "-d", |
| default=_PipelineSelection.NONE, |
| show_default=True, |
| type=FastEnumType( |
| _PipelineSelection, |
| [_PipelineSelection.BUILD, _PipelineSelection.RUN, _PipelineSelection.ALL, _PipelineSelection.NONE], |
| ), |
| help="The dependencies we also want to show", |
| ) |
| @click.argument("artifacts", type=click.Path(), nargs=-1) |
| @click.pass_obj |
| def artifact_show(app, deps, artifacts): |
| """show the cached state of artifacts""" |
| with app.initialized(): |
| targets = app.stream.artifact_show(artifacts, selection=deps) |
| click.echo(app.logger.show_state_of_artifacts(targets)) |
| sys.exit(0) |
| |
| |
| ##################################################################### |
| # Artifact Checkout Command # |
| ##################################################################### |
| @artifact.command(name="checkout", short_help="Checkout contents of an artifact") |
| @click.option("--force", "-f", is_flag=True, help="Allow files to be overwritten") |
| @click.option( |
| "--deps", |
| "-d", |
| default=_PipelineSelection.RUN, |
| show_default=True, |
| type=FastEnumType( |
| _PipelineSelection, |
| [_PipelineSelection.RUN, _PipelineSelection.BUILD, _PipelineSelection.NONE, _PipelineSelection.ALL], |
| ), |
| help="The dependencies to checkout", |
| ) |
| @click.option("--integrate/--no-integrate", default=None, is_flag=True, help="Whether to run integration commands") |
| @click.option("--hardlinks", is_flag=True, help="Checkout hardlinks instead of copying if possible") |
| @click.option( |
| "--tar", |
| default=None, |
| metavar="LOCATION", |
| type=click.Path(), |
| 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.option( |
| "--compression", |
| default=None, |
| type=click.Choice(["gz", "xz", "bz2"]), |
| help="The compression option of the tarball created.", |
| ) |
| @click.option("--pull", "pull_", is_flag=True, help="Pull the artifact if it's missing or incomplete.") |
| @click.option( |
| "--directory", default=None, type=click.Path(file_okay=False), help="The directory to checkout the artifact to" |
| ) |
| @click.option( |
| "--artifact-remote", |
| "artifact_remotes", |
| type=RemoteSpecType(RemoteSpecPurpose.PULL), |
| multiple=True, |
| help="A remote for downloading artifacts", |
| ) |
| @click.option( |
| "--ignore-project-artifact-remotes", |
| is_flag=True, |
| help="Ignore remote artifact cache servers recommended by projects", |
| ) |
| @click.argument("target", required=False, type=click.Path(readable=False)) |
| @click.pass_obj |
| def artifact_checkout( |
| app, |
| force, |
| deps, |
| integrate, |
| hardlinks, |
| tar, |
| compression, |
| pull_, |
| directory, |
| artifact_remotes, |
| ignore_project_artifact_remotes, |
| target, |
| ): |
| """Checkout contents of an artifact |
| |
| When this command is executed from a workspace directory, the default |
| is to checkout the artifact of the workspace element. |
| """ |
| from .. import utils |
| |
| if hardlinks and tar: |
| click.echo("ERROR: options --hardlinks and --tar conflict", err=True) |
| sys.exit(-1) |
| |
| if tar and directory: |
| click.echo("ERROR: options --directory and --tar conflict", err=True) |
| sys.exit(-1) |
| |
| if not tar: |
| if compression: |
| click.echo("ERROR: --compression can only be provided if --tar is provided", err=True) |
| sys.exit(-1) |
| else: |
| location = tar |
| try: |
| inferred_compression = utils._get_compression(tar) |
| except UtilError as e: |
| click.echo("ERROR: Invalid file extension given with '--tar': {}".format(e), err=True) |
| sys.exit(-1) |
| if compression and inferred_compression != "" and inferred_compression != compression: |
| click.echo( |
| "WARNING: File extension and compression differ." |
| "File extension has been overridden by --compression", |
| err=True, |
| ) |
| if not compression: |
| compression = inferred_compression |
| |
| with app.initialized(): |
| if not target: |
| target = app.project.get_default_target() |
| if not target: |
| raise AppError('Missing argument "ELEMENT".') |
| |
| if not tar: |
| if directory is None: |
| location = os.path.abspath(os.path.join(os.getcwd(), target)) |
| if location[-4:] == ".bst": |
| location = location[:-4] |
| else: |
| location = directory |
| |
| app.stream.checkout( |
| target, |
| location=location, |
| force=force, |
| selection=deps, |
| integrate=True if integrate is None else integrate, |
| hardlinks=hardlinks, |
| pull=pull_, |
| compression=compression, |
| tar=bool(tar), |
| artifact_remotes=artifact_remotes, |
| ignore_project_artifact_remotes=ignore_project_artifact_remotes, |
| ) |
| |
| |
| ################################################################ |
| # Artifact Pull Command # |
| ################################################################ |
| @artifact.command(name="pull", short_help="Pull a built artifact") |
| @click.option( |
| "--deps", |
| "-d", |
| default=_PipelineSelection.NONE, |
| show_default=True, |
| type=FastEnumType( |
| _PipelineSelection, |
| [_PipelineSelection.BUILD, _PipelineSelection.NONE, _PipelineSelection.RUN, _PipelineSelection.ALL], |
| ), |
| help="The dependency artifacts to pull", |
| ) |
| @click.option( |
| "--artifact-remote", |
| "artifact_remotes", |
| type=RemoteSpecType(RemoteSpecPurpose.PULL), |
| multiple=True, |
| help="A remote for downloading artifacts", |
| ) |
| @click.option( |
| "--ignore-project-artifact-remotes", |
| is_flag=True, |
| help="Ignore remote artifact cache servers recommended by projects", |
| ) |
| @click.argument("artifacts", nargs=-1, type=click.Path(readable=False)) |
| @click.pass_obj |
| def artifact_pull(app, deps, artifact_remotes, ignore_project_artifact_remotes, artifacts): |
| """Pull a built artifact from the configured remote artifact cache. |
| |
| Specifying no elements will result in pulling the default targets |
| of the project. If no default targets are configured, all project |
| elements will be pulled. |
| |
| When this command is executed from a workspace directory, the default |
| is to pull the workspace element. |
| |
| By default the artifact will be pulled one of the configured caches |
| if possible, following the usual priority order. If the `--artifact-remote` |
| flag is given, only the specified cache will be queried. |
| |
| Specify `--deps` to control which artifacts to pull: |
| |
| \b |
| none: No dependencies, just the element itself |
| run: Runtime dependencies, including the element itself |
| build: Build time dependencies, excluding the element itself |
| all: All dependencies |
| """ |
| |
| with app.initialized(session_name="Pull"): |
| ignore_junction_targets = False |
| |
| if not artifacts: |
| artifacts = app.project.get_default_targets() |
| # Junction elements cannot be pulled, exclude them from default targets |
| ignore_junction_targets = True |
| |
| app.stream.pull( |
| artifacts, |
| selection=deps, |
| ignore_junction_targets=ignore_junction_targets, |
| artifact_remotes=artifact_remotes, |
| ignore_project_artifact_remotes=ignore_project_artifact_remotes, |
| ) |
| |
| |
| ################################################################## |
| # Artifact Push Command # |
| ################################################################## |
| @artifact.command(name="push", short_help="Push a built artifact") |
| @click.option( |
| "--deps", |
| "-d", |
| default=_PipelineSelection.NONE, |
| show_default=True, |
| type=FastEnumType( |
| _PipelineSelection, |
| [_PipelineSelection.BUILD, _PipelineSelection.NONE, _PipelineSelection.RUN, _PipelineSelection.ALL], |
| ), |
| help="The dependencies to push", |
| ) |
| @click.option( |
| "--artifact-remote", |
| "artifact_remotes", |
| type=RemoteSpecType(RemoteSpecPurpose.PUSH), |
| multiple=True, |
| help="A remote for uploading artifacts", |
| ) |
| @click.option( |
| "--ignore-project-artifact-remotes", |
| is_flag=True, |
| help="Ignore remote artifact cache servers recommended by projects", |
| ) |
| @click.argument("artifacts", nargs=-1, type=click.Path(readable=False)) |
| @click.pass_obj |
| def artifact_push(app, deps, artifact_remotes, ignore_project_artifact_remotes, artifacts): |
| """Push built artifacts to a remote artifact cache, possibly pulling them first. |
| |
| Specifying no elements will result in pushing the default targets |
| of the project. If no default targets are configured, all project |
| elements will be pushed. |
| |
| When this command is executed from a workspace directory, the default |
| is to push the workspace element. |
| |
| If bst has been configured to include build trees on artifact pulls, |
| an attempt will be made to pull any required build trees to avoid the |
| skipping of partial artifacts being pushed. |
| |
| Specify `--deps` to control which artifacts to push: |
| |
| \b |
| none: No dependencies, just the element itself |
| run: Runtime dependencies, including the element itself |
| build: Build time dependencies, excluding the element itself |
| all: All dependencies |
| """ |
| with app.initialized(session_name="Push"): |
| ignore_junction_targets = False |
| |
| if not artifacts: |
| artifacts = app.project.get_default_targets() |
| # Junction elements cannot be pushed, exclude them from default targets |
| ignore_junction_targets = True |
| |
| app.stream.push( |
| artifacts, |
| selection=deps, |
| ignore_junction_targets=ignore_junction_targets, |
| artifact_remotes=artifact_remotes, |
| ignore_project_artifact_remotes=ignore_project_artifact_remotes, |
| ) |
| |
| |
| ################################################################ |
| # Artifact Log Command # |
| ################################################################ |
| @artifact.command(name="log", short_help="Show logs of artifacts") |
| @click.option( |
| "--out", |
| type=click.Path(file_okay=True, writable=True), |
| help="Output logs to individual files in the specified path. If absent, logs are written to stdout.", |
| ) |
| @click.argument("artifacts", type=click.Path(), nargs=-1) |
| @click.pass_obj |
| def artifact_log(app, artifacts, out): |
| """Show build logs of artifacts""" |
| with app.initialized(): |
| artifact_logs = app.stream.artifact_log(artifacts) |
| |
| if not out: |
| try: |
| for log in list(artifact_logs.values()): |
| with open(log[0], "r") as f: |
| data = f.read() |
| click.echo_via_pager(data) |
| except (OSError, FileNotFoundError): |
| click.echo("Error: file cannot be opened", err=True) |
| sys.exit(1) |
| |
| else: |
| try: |
| os.mkdir(out) |
| except FileExistsError: |
| click.echo("Error: {} already exists".format(out), err=True) |
| sys.exit(1) |
| |
| for name, log_files in artifact_logs.items(): |
| if len(log_files) > 1: |
| os.mkdir(name) |
| for log in log_files: |
| dest = os.path.join(out, name, log) |
| shutil.copy(log, dest) |
| # make a dir and write in log files |
| else: |
| log_name = os.path.splitext(name)[0] + ".log" |
| dest = os.path.join(out, log_name) |
| shutil.copy(log_files[0], dest) |
| # write a log file |
| |
| |
| ################################################################ |
| # Artifact List-Contents Command # |
| ################################################################ |
| @artifact.command(name="list-contents", short_help="List the contents of an artifact") |
| @click.option( |
| "--long", "-l", "long_", is_flag=True, help="Provide more information about the contents of the artifact." |
| ) |
| @click.argument("artifacts", type=click.Path(), nargs=-1) |
| @click.pass_obj |
| def artifact_list_contents(app, artifacts, long_): |
| """List the contents of an artifact. |
| |
| Note that 'artifacts' can be element names, which must end in '.bst', |
| or artifact references, which must be in the format `<project_name>/<element>/<key>`. |
| |
| """ |
| with app.initialized(): |
| elements_to_files = app.stream.artifact_list_contents(artifacts) |
| if not elements_to_files: |
| click.echo("None of the specified artifacts are cached.", err=True) |
| sys.exit(1) |
| else: |
| click.echo(app.logger._pretty_print_dictionary(elements_to_files, long_)) |
| sys.exit(0) |
| |
| |
| ################################################################### |
| # Artifact Delete Command # |
| ################################################################### |
| @artifact.command(name="delete", short_help="Remove artifacts from the local cache") |
| @click.option( |
| "--deps", |
| "-d", |
| default=_PipelineSelection.NONE, |
| show_default=True, |
| type=FastEnumType( |
| _PipelineSelection, |
| [_PipelineSelection.NONE, _PipelineSelection.RUN, _PipelineSelection.BUILD, _PipelineSelection.ALL], |
| ), |
| help="The dependencies to delete", |
| ) |
| @click.argument("artifacts", type=click.Path(), nargs=-1) |
| @click.pass_obj |
| def artifact_delete(app, artifacts, deps): |
| """Remove artifacts from the local cache""" |
| with app.initialized(): |
| app.stream.artifact_delete(artifacts, selection=deps) |