diff --git a/samcli/commands/build/build_context.py b/samcli/commands/build/build_context.py index fd8285accb2..8851670bf06 100644 --- a/samcli/commands/build/build_context.py +++ b/samcli/commands/build/build_context.py @@ -4,10 +4,12 @@ import copy import itertools +import json import logging import os import pathlib import shutil +import sys from collections import Counter from typing import Any, Dict, List, Optional, Tuple @@ -104,6 +106,7 @@ def __init__( mount_symlinks: Optional[bool] = False, use_buildkit: Optional[bool] = False, language_extensions: Optional[bool] = None, + output: str = "text", ) -> None: """ Initialize the class @@ -213,6 +216,7 @@ def __init__( self._mount_symlinks = mount_symlinks self._use_buildkit = use_buildkit self._language_extensions_enabled = resolve_language_extensions_enabled(language_extensions) + self._output = output def __enter__(self) -> "BuildContext": self.set_up() @@ -269,6 +273,9 @@ def get_resources_to_build(self): def run(self) -> None: """Runs the building process by creating an ApplicationBuilder.""" + if self._output == "json": + logging.getLogger("samcli").setLevel(logging.WARNING) + if self._is_sam_template(): SamApiProvider.check_implicit_api_resource_ids(self.stacks) @@ -321,8 +328,6 @@ def run(self) -> None: self._handle_build_post_processing(builder, self._build_result) - click.secho("\nBuild Succeeded", fg="green") - # try to use relpath so the command is easier to understand, however, # under Windows, when SAM and (build_dir or output_template_path) are # on different drive, relpath() fails. @@ -336,17 +341,43 @@ def run(self) -> None: build_dir_in_success_message = self.build_dir output_template_path_in_success_message = out_template_path - if self._print_success_message: - msg = self._gen_success_msg( - build_dir_in_success_message, - output_template_path_in_success_message, - os.path.abspath(self.build_dir) == os.path.abspath(DEFAULT_BUILD_DIR), - ) - - click.secho(msg, fg="yellow") + if self._output == "json": + result = { + "status": "success", + "build_dir": build_dir_in_success_message, + "template_file": output_template_path_in_success_message, + "resources": [ + { + "logical_id": f.full_path, + "runtime": f.runtime, + "architecture": f.architectures[0] if f.architectures else None, + } + for f in self.get_resources_to_build().functions + ], + } + click.echo(json.dumps(result, indent=2)) + else: + click.secho("\nBuild Succeeded", fg="green") + if self._print_success_message: + msg = self._gen_success_msg( + build_dir_in_success_message, + output_template_path_in_success_message, + os.path.abspath(self.build_dir) == os.path.abspath(DEFAULT_BUILD_DIR), + ) + click.secho(msg, fg="yellow") except FunctionNotFound as function_not_found_ex: caught_exception = function_not_found_ex + if self._output == "json": + error_result = { + "status": "failure", + "error": { + "type": "FunctionNotFound", + "message": str(function_not_found_ex), + }, + } + click.echo(json.dumps(error_result, indent=2)) + sys.exit(1) raise UserException( str(function_not_found_ex), wrapped_from=function_not_found_ex.__class__.__name__ ) from function_not_found_ex @@ -360,6 +391,22 @@ def run(self) -> None: ) as ex: caught_exception = ex + if self._output == "json": + deep_wrap = getattr(ex, "wrapped_from", None) + error_type = deep_wrap if deep_wrap else ex.__class__.__name__ + error_result = { + "status": "failure", + "error": { + "type": error_type, + "message": str(ex), + }, + } + resource_name = getattr(ex, "resource_name", None) + if resource_name: + error_result["error"]["resource"] = resource_name + click.echo(json.dumps(error_result, indent=2)) + sys.exit(1) + click.secho("\nBuild Failed", fg="red") # Some Exceptions have a deeper wrapped exception that needs to be surfaced diff --git a/samcli/commands/build/command.py b/samcli/commands/build/command.py index 8ec13607e70..cea6c08e1a5 100644 --- a/samcli/commands/build/command.py +++ b/samcli/commands/build/command.py @@ -129,6 +129,13 @@ @template_option_without_build @parameter_override_option @docker_common_options +@click.option( + "--output", + default="text", + help="Output the results from the command in a given output format. " + "Supported formats: text (default), json.", + type=click.Choice(["text", "json"], case_sensitive=False), +) @cli_framework_options @aws_creds_options @click.argument("resource_logical_id", required=False) @@ -167,6 +174,7 @@ def cli( mount_symlinks: Optional[bool], use_buildkit: Optional[bool], language_extensions: Optional[bool], + output: str, ) -> None: """ `sam build` command entry point @@ -201,6 +209,7 @@ def cli( mount_symlinks, use_buildkit, language_extensions, + output, ) # pragma: no cover @@ -230,6 +239,7 @@ def do_cli( # pylint: disable=too-many-locals, too-many-statements mount_symlinks: Optional[bool], use_buildkit: Optional[bool], language_extensions: Optional[bool], + output: str = "text", ) -> None: """ Implementation of the ``cli`` method @@ -272,6 +282,7 @@ def do_cli( # pylint: disable=too-many-locals, too-many-statements mount_symlinks=mount_symlinks, use_buildkit=use_buildkit, language_extensions=language_extensions, + output=output, ) as ctx: ctx.run() diff --git a/samcli/commands/build/core/options.py b/samcli/commands/build/core/options.py index 4b86570817f..01d6af38d09 100644 --- a/samcli/commands/build/core/options.py +++ b/samcli/commands/build/core/options.py @@ -30,7 +30,7 @@ EXTENSION_OPTIONS: List[str] = ["hook_name", "skip_prepare_infra"] -BUILD_STRATEGY_OPTIONS: List[str] = ["parallel", "exclude", "manifest", "cached", "build_in_source"] +BUILD_STRATEGY_OPTIONS: List[str] = ["parallel", "exclude", "manifest", "cached", "build_in_source", "output"] ARTIFACT_LOCATION_OPTIONS: List[str] = [ "build_dir", diff --git a/samcli/lib/build/app_builder.py b/samcli/lib/build/app_builder.py index c30c948c007..aae3dcd0243 100644 --- a/samcli/lib/build/app_builder.py +++ b/samcli/lib/build/app_builder.py @@ -807,19 +807,23 @@ def _build_function( # pylint: disable=R1710 specified_workflow=specified_workflow if supported_specified_workflow else None, ) - return self._build_function_in_process( - config, - code_dir, - artifact_dir, - scratch_dir, - manifest_path, - runtime, - architecture, - options, - dependencies_dir, - download_dependencies, - self._combine_dependencies, - ) + try: + return self._build_function_in_process( + config, + code_dir, + artifact_dir, + scratch_dir, + manifest_path, + runtime, + architecture, + options, + dependencies_dir, + download_dependencies, + self._combine_dependencies, + ) + except BuildError as ex: + ex.resource_name = function_name + raise # pylint: disable=fixme # FIXME: we need to throw an exception here, packagetype could be something else diff --git a/samcli/lib/build/exceptions.py b/samcli/lib/build/exceptions.py index e1f4ff50fcc..1526501f7c1 100644 --- a/samcli/lib/build/exceptions.py +++ b/samcli/lib/build/exceptions.py @@ -2,6 +2,8 @@ Build Related Exceptions. """ +from typing import Optional + from samcli.commands.exceptions import UserException @@ -15,8 +17,9 @@ def __init__(self, container_name: str, error_msg: str) -> None: class BuildError(Exception): - def __init__(self, wrapped_from: str, msg: str) -> None: + def __init__(self, wrapped_from: str, msg: str, resource_name: Optional[str] = None) -> None: self.wrapped_from = wrapped_from + self.resource_name = resource_name Exception.__init__(self, msg)