holoscan-cli 2.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. holoscan_cli/__init__.py +35 -0
  2. holoscan_cli/__main__.py +164 -0
  3. holoscan_cli/common/argparse_types.py +156 -0
  4. holoscan_cli/common/artifact_sources.py +160 -0
  5. holoscan_cli/common/constants.py +119 -0
  6. holoscan_cli/common/dockerutils.py +521 -0
  7. holoscan_cli/common/enum_types.py +49 -0
  8. holoscan_cli/common/exceptions.py +126 -0
  9. holoscan_cli/common/sdk_utils.py +195 -0
  10. holoscan_cli/common/utils.py +137 -0
  11. holoscan_cli/logging.json +37 -0
  12. holoscan_cli/nics/__init__.py +15 -0
  13. holoscan_cli/nics/nics.py +33 -0
  14. holoscan_cli/package-source.json +32 -0
  15. holoscan_cli/packager/__init__.py +15 -0
  16. holoscan_cli/packager/arguments.py +148 -0
  17. holoscan_cli/packager/config_reader.py +180 -0
  18. holoscan_cli/packager/container_builder.py +426 -0
  19. holoscan_cli/packager/manifest_files.py +217 -0
  20. holoscan_cli/packager/models.py +90 -0
  21. holoscan_cli/packager/package_command.py +197 -0
  22. holoscan_cli/packager/packager.py +124 -0
  23. holoscan_cli/packager/parameters.py +603 -0
  24. holoscan_cli/packager/platforms.py +426 -0
  25. holoscan_cli/packager/templates/Dockerfile.jinja2 +479 -0
  26. holoscan_cli/packager/templates/dockerignore +92 -0
  27. holoscan_cli/packager/templates/tools.sh +414 -0
  28. holoscan_cli/py.typed +14 -0
  29. holoscan_cli/runner/__init__.py +15 -0
  30. holoscan_cli/runner/resources.py +185 -0
  31. holoscan_cli/runner/run_command.py +207 -0
  32. holoscan_cli/runner/runner.py +340 -0
  33. holoscan_cli/version/__init__.py +15 -0
  34. holoscan_cli/version/version.py +53 -0
  35. holoscan_cli-2.9.0.dist-info/LICENSE +201 -0
  36. holoscan_cli-2.9.0.dist-info/METADATA +102 -0
  37. holoscan_cli-2.9.0.dist-info/RECORD +39 -0
  38. holoscan_cli-2.9.0.dist-info/WHEEL +4 -0
  39. holoscan_cli-2.9.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,217 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+
19
+ class ApplicationManifest:
20
+ def __init__(self):
21
+ self._data = {}
22
+ self._data["apiVersion"] = None
23
+ self._data["command"] = None
24
+ self._data["environment"] = None
25
+ self._data["input"] = None
26
+ self._data["liveness"] = None
27
+ self._data["output"] = None
28
+ self._data["readiness"] = None
29
+ self._data["sdk"] = None
30
+ self._data["sdkVersion"] = None
31
+ self._data["timeout"] = None
32
+ self._data["version"] = None
33
+ self._data["workingDirectory"] = None
34
+
35
+ @property
36
+ def api_version(self) -> str:
37
+ return self._data["apiVersion"]
38
+
39
+ @api_version.setter
40
+ def api_version(self, value: str):
41
+ self._data["apiVersion"] = value
42
+
43
+ @property
44
+ def command(self) -> str:
45
+ return self._data["command"]
46
+
47
+ @command.setter
48
+ def command(self, value: str):
49
+ self._data["command"] = value
50
+
51
+ @property
52
+ def environment(self) -> dict[str, str]:
53
+ return self._data["environment"]
54
+
55
+ @environment.setter
56
+ def environment(self, value: dict[str, str]):
57
+ self._data["environment"] = value
58
+
59
+ @property
60
+ def input(self) -> dict[str, str]: # noqa: A003
61
+ return self._data["input"]
62
+
63
+ @input.setter
64
+ def input(self, value: dict[str, str]): # noqa: A003
65
+ self._data["input"] = value
66
+
67
+ @property
68
+ def liveness(self) -> Any:
69
+ return self._data["liveness"]
70
+
71
+ @liveness.setter
72
+ def liveness(self, value: Any):
73
+ self._data["liveness"] = value
74
+
75
+ @property
76
+ def output(self) -> dict[str, str]:
77
+ return self._data["output"]
78
+
79
+ @output.setter
80
+ def output(self, value: dict[str, str]):
81
+ self._data["output"] = value
82
+
83
+ @property
84
+ def readiness(self) -> Any:
85
+ return self._data["readiness"]
86
+
87
+ @readiness.setter
88
+ def readiness(self, value: Any):
89
+ self._data["readiness"] = value
90
+
91
+ @property
92
+ def sdk(self) -> str:
93
+ return self._data["sdk"]
94
+
95
+ @sdk.setter
96
+ def sdk(self, value: str):
97
+ self._data["sdk"] = value
98
+
99
+ @property
100
+ def sdk_version(self) -> str:
101
+ return self._data["sdkVersion"]
102
+
103
+ @sdk_version.setter
104
+ def sdk_version(self, value: str):
105
+ self._data["sdkVersion"] = value
106
+
107
+ @property
108
+ def timeout(self) -> int:
109
+ return self._data["timeout"]
110
+
111
+ @timeout.setter
112
+ def timeout(self, value: int):
113
+ self._data["timeout"] = value
114
+
115
+ @property
116
+ def version(self) -> str:
117
+ return self._data["version"]
118
+
119
+ @version.setter
120
+ def version(self, value: str):
121
+ self._data["version"] = value
122
+
123
+ @property
124
+ def working_directory(self) -> str:
125
+ return self._data["workingDirectory"]
126
+
127
+ @working_directory.setter
128
+ def working_directory(self, value: str):
129
+ if isinstance(value, Path):
130
+ self._data["workingDirectory"] = str(value)
131
+ else:
132
+ self._data["workingDirectory"] = value
133
+
134
+ @property
135
+ def data(self) -> dict[str, Any]:
136
+ """Returns all values for serializing to JSON"""
137
+ return self._data
138
+
139
+
140
+ class PackageManifest:
141
+ def __init__(self):
142
+ self._data = {}
143
+ self._data["apiVersion"] = None
144
+ self._data["applicationRoot"] = None
145
+ self._data["modelRoot"] = None
146
+ self._data["models"] = None
147
+ self._data["resources"] = None
148
+ self._data["version"] = None
149
+ self._data["platformConfig"] = None
150
+
151
+ @property
152
+ def platform_config(self) -> str:
153
+ return self._data["platformConfig"]
154
+
155
+ @platform_config.setter
156
+ def platform_config(self, value: str):
157
+ self._data["platformConfig"] = value
158
+
159
+ @property
160
+ def api_version(self) -> str:
161
+ return self._data["apiVersion"]
162
+
163
+ @api_version.setter
164
+ def api_version(self, value: str):
165
+ self._data["apiVersion"] = value
166
+
167
+ @property
168
+ def application_root(self) -> str:
169
+ return self._data["applicationRoot"]
170
+
171
+ @application_root.setter
172
+ def application_root(self, value: str):
173
+ if isinstance(value, Path):
174
+ self._data["applicationRoot"] = str(value)
175
+ else:
176
+ self._data["applicationRoot"] = value
177
+
178
+ @property
179
+ def model_root(self) -> str:
180
+ return self._data["modelRoot"]
181
+
182
+ @model_root.setter
183
+ def model_root(self, value: str):
184
+ if isinstance(value, Path):
185
+ self._data["modelRoot"] = str(value)
186
+ else:
187
+ self._data["modelRoot"] = value
188
+
189
+ @property
190
+ def models(self) -> dict[str, str]:
191
+ return self._data["models"]
192
+
193
+ @models.setter
194
+ def models(self, value: dict[str, str]):
195
+ self._data["models"] = value
196
+
197
+ @property
198
+ def resources(self) -> Any:
199
+ return self._data["resources"]
200
+
201
+ @resources.setter
202
+ def resources(self, value: Any):
203
+ """Resources are copied from application configuration file directly."""
204
+ self._data["resources"] = value
205
+
206
+ @property
207
+ def version(self) -> str:
208
+ return self._data["version"]
209
+
210
+ @version.setter
211
+ def version(self, value: str):
212
+ self._data["version"] = value
213
+
214
+ @property
215
+ def data(self) -> dict[str, Any]:
216
+ """Returns all values for serializing to JSON"""
217
+ return self._data
@@ -0,0 +1,90 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ import logging
16
+ import os
17
+ from pathlib import Path
18
+ from typing import Optional
19
+
20
+ logger = logging.getLogger("packager")
21
+
22
+
23
+ class Models:
24
+ """
25
+ Builds model list from a given path where the path could be a model file, a directory that
26
+ contains a model or a directory with multiple models where each model contains within its own
27
+ subdirectory.
28
+ """
29
+
30
+ def build(self, models_path: Path) -> Optional[dict[str, Path]]:
31
+ """Checks if the given path is a file or a directory.
32
+
33
+ Args:
34
+ models_path (Path): A user given path that contains one or more models.
35
+ Returns:
36
+ Optional[Dict[str, Path]]: Returns None if no path is given. Otherwise, returns a
37
+ dictionary where the key is the name of the model and the value contains the path
38
+ to the model.
39
+ """
40
+ if models_path is not None:
41
+ logger.info(f"Scanning for models in {models_path}...")
42
+ models: dict[str, Path] = {}
43
+ if models_path.is_file():
44
+ self._configure_model_file(models_path, models)
45
+ elif models_path.is_dir():
46
+ self._configure_model_dir(models_path, models)
47
+
48
+ return models
49
+ else:
50
+ return None
51
+
52
+ def _configure_model_dir(self, models_path: Path, models: dict[str, Path]):
53
+ """
54
+ Iterate through the given directory to scan for models.
55
+ If files are found within the directory, we simply assume that all files within the given
56
+ directory contains a model and sets the name of the model to the name of the given
57
+ directory.
58
+
59
+ Any subdirectories found within the given directory are treated as a separate model and the
60
+ name of the subdirectory is set to be the name of the model.
61
+
62
+ Args:
63
+ models_path (Path): Path to the model file.
64
+ models (Dict[str, Path]): Where models are added to.
65
+ """
66
+ model_dirs = os.listdir(models_path)
67
+
68
+ for model_dir in model_dirs:
69
+ if os.path.isfile(models_path / model_dir):
70
+ model_name = models_path.resolve().stem
71
+ models[model_name] = models_path
72
+ logger.debug(f"Model {model_name}={models_path} added.")
73
+ elif os.path.isdir(models_path / model_dir):
74
+ model_path = models_path / model_dir
75
+ model_name = model_dir
76
+ models[model_name] = model_path
77
+ logger.debug(f"Model {model_name}={model_path} added.")
78
+
79
+ def _configure_model_file(self, models_path: Path, models: dict[str, Path]):
80
+ """
81
+ Adds a new model to 'models' object where the model name is the name of the given file,
82
+ and the value is the path to the given model file.
83
+
84
+ Args:
85
+ models_path (Path): Path to the model file.
86
+ models (Dict[str, Path]): Where the model is added to.
87
+ """
88
+ model_name = models_path.stem
89
+ models[model_name] = models_path
90
+ logger.debug(f"Model file {model_name}={models[model_name]} added.")
@@ -0,0 +1,197 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ import argparse
16
+ import logging
17
+ from argparse import ArgumentParser, _SubParsersAction
18
+
19
+ from packaging.version import Version
20
+
21
+ from ..common.argparse_types import (
22
+ valid_dir_path,
23
+ valid_existing_dir_path,
24
+ valid_existing_path,
25
+ valid_platform_config,
26
+ valid_platforms,
27
+ valid_sdk_type,
28
+ )
29
+ from ..common.constants import SDK
30
+
31
+ logger = logging.getLogger("packager")
32
+
33
+
34
+ def create_package_parser(
35
+ subparser: _SubParsersAction, command: str, parents: list[ArgumentParser]
36
+ ) -> ArgumentParser:
37
+ parser: ArgumentParser = subparser.add_parser(
38
+ command, formatter_class=argparse.HelpFormatter, parents=parents, add_help=False
39
+ )
40
+
41
+ parser.add_argument(
42
+ "application",
43
+ type=valid_existing_path,
44
+ help="Holoscan application path: Python application directory with __main__.py, "
45
+ "Python file, C++ source directory with CMakeLists.txt, or path to an executable.",
46
+ )
47
+ parser.add_argument(
48
+ "--config",
49
+ "-c",
50
+ required=True,
51
+ type=valid_existing_path,
52
+ help="Holoscan application configuration file (.yaml)",
53
+ )
54
+ parser.add_argument(
55
+ "--docs",
56
+ "-d",
57
+ type=valid_existing_dir_path,
58
+ help="path to a directory containing user documentation and/or licenses.",
59
+ )
60
+ parser.add_argument(
61
+ "--models",
62
+ "-m",
63
+ type=valid_existing_path,
64
+ help="path to a model file or a directory containing all models as subdirectories",
65
+ )
66
+ parser.add_argument(
67
+ "--platform",
68
+ type=valid_platforms,
69
+ required=True,
70
+ help="target platform(s) for the build output separated by comma. "
71
+ f"Valid values: {str.join(', ', SDK.PLATFORMS)}.",
72
+ )
73
+ parser.add_argument(
74
+ "--platform-config",
75
+ type=valid_platform_config,
76
+ help="target platform configuration for the build output. "
77
+ f"Valid values: {str.join(', ', SDK.PLATFORM_CONFIGS)}.",
78
+ )
79
+ parser.add_argument(
80
+ "--add",
81
+ action="append",
82
+ dest="additional_libs",
83
+ type=valid_existing_dir_path,
84
+ help="include additional library files, python files into the application directory.",
85
+ )
86
+ parser.add_argument(
87
+ "--timeout", type=int, help="override default application timeout"
88
+ )
89
+ parser.add_argument(
90
+ "--version", type=Version, help="set the version of the application"
91
+ )
92
+
93
+ advanced_group = parser.add_argument_group(title="advanced build options")
94
+ advanced_group.add_argument(
95
+ "--base-image",
96
+ type=str,
97
+ help="base image name for the packaged application.",
98
+ )
99
+ advanced_group.add_argument(
100
+ "--build-image",
101
+ type=str,
102
+ help="container image name for building the C++ application.",
103
+ )
104
+ advanced_group.add_argument(
105
+ "--includes",
106
+ nargs="*",
107
+ default=[],
108
+ choices=["debug", "holoviz", "torch", "onnx"],
109
+ help="additional packages to include in the container.",
110
+ )
111
+ advanced_group.add_argument(
112
+ "--build-cache",
113
+ type=valid_dir_path,
114
+ default="~/.holoscan_build_cache",
115
+ help="path of the local directory where build cache gets stored.",
116
+ )
117
+ advanced_group.add_argument(
118
+ "--cmake-args",
119
+ type=str,
120
+ help='additional CMAKE build arguments. E.g. "-DCMAKE_BUILD_TYPE=DEBUG -DCMAKE_ARG=VALUE"',
121
+ )
122
+ advanced_group.add_argument(
123
+ "--holoscan-sdk-file",
124
+ type=valid_existing_path,
125
+ help="path to the Holoscan SDK Debian or PyPI package. "
126
+ "If not specified, the packager downloads "
127
+ "the SDK file from the internet based on the SDK version.",
128
+ )
129
+ advanced_group.add_argument(
130
+ "--monai-deploy-sdk-file",
131
+ type=valid_existing_path,
132
+ help="path to the MONAI Deploy App SDK PyPI package. "
133
+ "If not specified, the packager downloads "
134
+ "the SDK file from the internet based on the SDK version.",
135
+ )
136
+ advanced_group.add_argument(
137
+ "--no-cache",
138
+ "-n",
139
+ dest="no_cache",
140
+ action="store_true",
141
+ help="do not use cache when building image",
142
+ )
143
+ advanced_group.add_argument(
144
+ "--sdk",
145
+ type=valid_sdk_type,
146
+ help="SDK for building the application: Holoscan or MONAI-Deploy. "
147
+ f"Valid values: {str.join(', ', SDK.SDKS)}.",
148
+ )
149
+ advanced_group.add_argument(
150
+ "--source",
151
+ type=str,
152
+ help="override Debian package, build container image and run container image from a "
153
+ "JSON formatted file or a secured web server (HTTPS).",
154
+ )
155
+ advanced_group.add_argument(
156
+ "--sdk-version",
157
+ type=Version,
158
+ help="set the version of the SDK that is used to build and package the application. "
159
+ "If not specified, the packager attempts to detect the installed version.",
160
+ )
161
+
162
+ output_group = parser.add_argument_group(title="output options")
163
+ output_group.add_argument(
164
+ "--output",
165
+ "-o",
166
+ type=valid_existing_dir_path,
167
+ help="output directory where result images will be written.",
168
+ )
169
+ output_group.add_argument(
170
+ "--tag",
171
+ "-t",
172
+ required=True,
173
+ type=str,
174
+ help="name of the image and a optional tag (format: name[:tag]).",
175
+ )
176
+
177
+ user_group = parser.add_argument_group(title="security options")
178
+ user_group.add_argument(
179
+ "--username",
180
+ type=str,
181
+ default="holoscan",
182
+ help="username to be created in the container execution context.",
183
+ )
184
+ user_group.add_argument(
185
+ "--uid",
186
+ type=str,
187
+ default=1000,
188
+ help="UID associated with the username. (default:1000)",
189
+ )
190
+ user_group.add_argument(
191
+ "--gid",
192
+ type=str,
193
+ default=1000,
194
+ help="GID associated with the username. (default:1000)",
195
+ )
196
+ parser.set_defaults(no_cache=False)
197
+ return parser
@@ -0,0 +1,124 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ import json
16
+ import logging
17
+ import os
18
+ import sys
19
+ import tempfile
20
+ from argparse import Namespace
21
+
22
+ from ..common.enum_types import ApplicationType
23
+ from ..common.utils import print_manifest_json
24
+ from .arguments import PackagingArguments
25
+ from .container_builder import CppAppBuilder, PythonAppBuilder
26
+ from .manifest_files import ApplicationManifest, PackageManifest
27
+ from .parameters import PlatformBuildResults
28
+
29
+ logger = logging.getLogger("packager")
30
+
31
+
32
+ def _build_image(args: PackagingArguments, temp_dir: str) -> list[PlatformBuildResults]:
33
+ """Creates dockerfile and builds HAP/MONAI Application Package (HAP/MAP) image
34
+ Args:
35
+ args (dict): Input arguments for Packager
36
+ temp_dir (str): Temporary directory to build MAP
37
+ """
38
+ if (
39
+ args.build_parameters.application_type == ApplicationType.PythonFile
40
+ or args.build_parameters.application_type == ApplicationType.PythonModule
41
+ ):
42
+ builder = PythonAppBuilder(args.build_parameters, temp_dir)
43
+ elif (
44
+ args.build_parameters.application_type == ApplicationType.Binary
45
+ or args.build_parameters.application_type == ApplicationType.CppCMake
46
+ ):
47
+ builder = CppAppBuilder(args.build_parameters, temp_dir)
48
+
49
+ results = [builder.build(platform) for platform in args.platforms]
50
+ return results
51
+
52
+
53
+ def _create_app_manifest(manifest: ApplicationManifest, temp_dir: str):
54
+ """Creates Application manifest .json file
55
+ Args:
56
+ manifest (Dict): Input arguments for Packager
57
+ temp_dir (str): Temporary directory to build MAP
58
+ """
59
+ map_folder_path = os.path.join(temp_dir, "map")
60
+ os.makedirs(map_folder_path, exist_ok=True)
61
+ with open(os.path.join(map_folder_path, "app.json"), "w") as app_manifest_file:
62
+ app_manifest_file.write(json.dumps(manifest.data))
63
+
64
+ print_manifest_json(manifest.data, "app.json")
65
+
66
+
67
+ def _create_package_manifest(manifest: PackageManifest, temp_dir: str):
68
+ """Creates package manifest .json file
69
+ Args:
70
+ manifest (Dict): Input arguments for Packager
71
+ temp_dir (str): Temporary directory to build MAP
72
+ """
73
+ map_folder_path = os.path.join(temp_dir, "map")
74
+ os.makedirs(map_folder_path, exist_ok=True)
75
+ with open(os.path.join(temp_dir, "map", "pkg.json"), "w") as package_manifest_file:
76
+ package_manifest_file.write(json.dumps(manifest.data))
77
+
78
+ print_manifest_json(manifest.data, "pkg.json")
79
+
80
+
81
+ def _package_application(args: Namespace):
82
+ """Driver function for invoking all functions for creating and
83
+ building the Holoscan Application package image
84
+ Args:
85
+ args (Namespace): Input arguments for Packager from CLI
86
+ """
87
+ # Initialize arguments for package
88
+ with tempfile.TemporaryDirectory(
89
+ prefix="holoscan_tmp", dir=tempfile.gettempdir()
90
+ ) as temp_dir:
91
+ packaging_args = PackagingArguments(args, temp_dir)
92
+
93
+ # Create Manifest Files
94
+ _create_app_manifest(packaging_args.application_manifest, temp_dir)
95
+ _create_package_manifest(packaging_args.package_manifest, temp_dir)
96
+
97
+ results = _build_image(packaging_args, temp_dir)
98
+
99
+ logger.info("Build Summary:")
100
+ for result in results:
101
+ if result.succeeded:
102
+ print(
103
+ f"""\nPlatform: {result.parameters.platform.value}/{result.parameters.platform_config.value}
104
+ Status: Succeeded
105
+ Docker Tag: {result.docker_tag if result.docker_tag is not None else "N/A"}
106
+ Tarball: {result.tarball_filenaem}""" # noqa: E501
107
+ )
108
+ else:
109
+ print(
110
+ f"""\nPlatform: {result.parameters.platform.value}/{result.parameters.platform_config.value}
111
+ Status: Failure
112
+ Error: {result.error}
113
+ """ # noqa: E501
114
+ )
115
+ sys.exit(1)
116
+
117
+
118
+ def execute_package_command(args: Namespace):
119
+ try:
120
+ _package_application(args)
121
+ except Exception as e:
122
+ logger.debug(e, exc_info=True)
123
+ logger.error(f"Error packaging application:\n\n{str(e)}")
124
+ sys.exit(1)