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,180 @@
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 os
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ import yaml
20
+
21
+ from ..common.constants import DefaultValues, EnvironmentVariables
22
+ from ..common.exceptions import InvalidApplicationConfigurationError
23
+ from .manifest_files import ApplicationManifest, PackageManifest
24
+ from .parameters import PackageBuildParameters
25
+
26
+
27
+ class ApplicationConfiguration:
28
+ def __init__(self) -> None:
29
+ self._config = None
30
+
31
+ def read(self, path: Path):
32
+ """Reads application configuration file
33
+
34
+ Args:
35
+ path (Path): path to the app configuration file
36
+
37
+ Raises:
38
+ FileNotFoundError: when file does not exists or is not a file
39
+ InvalidApplicationConfigurationError: error reading file
40
+ """
41
+ if not path.exists() or not path.is_file():
42
+ raise FileNotFoundError(
43
+ f"Specified application configuration file cannot be found: {path}"
44
+ )
45
+
46
+ try:
47
+ with open(path) as app_manifest_file:
48
+ self._config = yaml.load(app_manifest_file, yaml.SafeLoader)
49
+ except Exception as ex:
50
+ raise InvalidApplicationConfigurationError(
51
+ f"Error reading application configuration file from '{path}'. Please check that "
52
+ "the file is accessible and is a valid YAML file.",
53
+ ex,
54
+ ) from ex
55
+
56
+ self._config_file_path = path
57
+ self._validate()
58
+
59
+ def _validate(self):
60
+ if self._config is None:
61
+ raise InvalidApplicationConfigurationError(
62
+ f"Error reading application configuration: {self._config_file_path}"
63
+ )
64
+
65
+ if "application" not in self._config:
66
+ raise InvalidApplicationConfigurationError(
67
+ "Application ('application') configuration cannot be found in "
68
+ f"{self._config_file_path}"
69
+ )
70
+ if "resources" not in self._config:
71
+ raise InvalidApplicationConfigurationError(
72
+ "Resources ('resources') configuration cannot be found in "
73
+ f"{self._config_file_path}"
74
+ )
75
+ if (
76
+ "title" not in self._config["application"]
77
+ or len(self._config["application"]["title"]) <= 0
78
+ ):
79
+ raise InvalidApplicationConfigurationError(
80
+ "Application configuration key/value ('application>title') "
81
+ f"cannot be found or is empty in {self._config_file_path}"
82
+ )
83
+ self._application_object = self._config["application"]
84
+ self._resource_object = self._config["resources"]
85
+
86
+ def title(self) -> str:
87
+ return self._application_object["title"]
88
+
89
+ def pip_packages(self) -> Any:
90
+ if "pip-packages" in self._application_object:
91
+ return self._application_object["pip-packages"]
92
+ return None
93
+
94
+ def populate_app_manifest(
95
+ self, build_parameters: PackageBuildParameters
96
+ ) -> ApplicationManifest:
97
+ application_manifest = ApplicationManifest()
98
+ application_manifest.api_version = DefaultValues.API_VERSION
99
+ application_manifest.command = build_parameters.command
100
+
101
+ application_manifest.environment = {}
102
+ application_manifest.environment[EnvironmentVariables.HOLOSCAN_APPLICATION] = (
103
+ str(build_parameters.app_dir)
104
+ )
105
+ application_manifest.environment[EnvironmentVariables.HOLOSCAN_INPUT_PATH] = (
106
+ str(build_parameters.input_dir)
107
+ )
108
+ application_manifest.environment[EnvironmentVariables.HOLOSCAN_OUTPUT_PATH] = (
109
+ str(build_parameters.output_dir)
110
+ )
111
+ application_manifest.environment[EnvironmentVariables.HOLOSCAN_WORKDIR] = str(
112
+ build_parameters.working_dir
113
+ )
114
+ application_manifest.environment[EnvironmentVariables.HOLOSCAN_MODEL_PATH] = (
115
+ str(build_parameters.models_dir)
116
+ )
117
+ application_manifest.environment[EnvironmentVariables.HOLOSCAN_CONFIG_PATH] = (
118
+ str(build_parameters.config_file_path)
119
+ )
120
+ application_manifest.environment[
121
+ EnvironmentVariables.HOLOSCAN_APP_MANIFEST_PATH
122
+ ] = str(build_parameters.app_manifest_path)
123
+ application_manifest.environment[
124
+ EnvironmentVariables.HOLOSCAN_PKG_MANIFEST_PATH
125
+ ] = str(build_parameters.package_manifest_path)
126
+ application_manifest.environment[EnvironmentVariables.HOLOSCAN_DOCS_PATH] = str(
127
+ build_parameters.docs_dir
128
+ )
129
+ application_manifest.environment[EnvironmentVariables.HOLOSCAN_LOGS_PATH] = str(
130
+ build_parameters.logs_dir
131
+ )
132
+
133
+ application_manifest.input = {
134
+ "path": build_parameters.input_dir,
135
+ "formats": self._application_object.get("input-formats", None),
136
+ }
137
+
138
+ application_manifest.output = {
139
+ "path": build_parameters.output_dir,
140
+ "formats": self._application_object.get("output-formats", None),
141
+ }
142
+
143
+ application_manifest.readiness = None
144
+ application_manifest.liveness = None
145
+ application_manifest.timeout = build_parameters.timeout
146
+ application_manifest.version = self._get_version(build_parameters)
147
+ application_manifest.working_directory = str(build_parameters.working_dir)
148
+
149
+ return application_manifest
150
+
151
+ def populate_package_manifest(
152
+ self, build_parameters: PackageBuildParameters
153
+ ) -> PackageManifest:
154
+ package_manifest = PackageManifest()
155
+ package_manifest.api_version = DefaultValues.API_VERSION
156
+ package_manifest.application_root = str(build_parameters.app_dir)
157
+ package_manifest.model_root = str(build_parameters.models_dir)
158
+
159
+ package_manifest.models = {}
160
+ if build_parameters.models is not None:
161
+ for model in build_parameters.models:
162
+ package_manifest.models[model] = os.path.join(
163
+ package_manifest.model_root, model
164
+ )
165
+ package_manifest.resources = self._resource_object
166
+ package_manifest.version = self._get_version(build_parameters)
167
+
168
+ return package_manifest
169
+
170
+ def _get_version(self, build_parameters: PackageBuildParameters) -> str:
171
+ if build_parameters.version is None:
172
+ if "version" not in self._application_object:
173
+ raise InvalidApplicationConfigurationError(
174
+ "Application configuration key/value ('application>version') "
175
+ f"cannot be found or is empty in {self._config_file_path}"
176
+ )
177
+
178
+ return self._application_object["version"]
179
+
180
+ return build_parameters.version
@@ -0,0 +1,426 @@
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
+ import pprint
18
+ import shutil
19
+ from pathlib import Path
20
+ from typing import Optional
21
+
22
+ from jinja2 import Environment, FileSystemLoader, StrictUndefined
23
+
24
+ from ..common.constants import Constants, DefaultValues
25
+ from ..common.dockerutils import (
26
+ build_docker_image,
27
+ create_and_get_builder,
28
+ docker_export_tarball,
29
+ )
30
+ from ..common.exceptions import WrongApplicationPathError
31
+ from .parameters import PackageBuildParameters, PlatformBuildResults, PlatformParameters
32
+
33
+
34
+ class BuilderBase:
35
+ """
36
+ Docker container image builder base class.
37
+ Prepares files for building the docker image and calls Docker API to build the container image.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ build_parameters: PackageBuildParameters,
43
+ temp_dir: str,
44
+ ) -> None:
45
+ """
46
+ Copy the application, model files, and user documentations here in __init__ since they
47
+ won't change when building different platforms.
48
+
49
+ Args:
50
+ build_parameters (PackageBuildParameters): general build parameters
51
+ temp_dir (str): temporary directory to store files required for build
52
+
53
+ """
54
+ self._logger = logging.getLogger("packager.builder")
55
+ self._build_parameters = build_parameters
56
+ self._temp_dir = temp_dir
57
+ self._copy_application()
58
+ self._copy_model_files()
59
+ self._copy_docs()
60
+ self._copy_libs()
61
+ _ = self._write_dockerignore()
62
+ _ = self._copy_script()
63
+
64
+ def build(self, platform_parameters: PlatformParameters) -> PlatformBuildResults:
65
+ """Build a new container image for a specific platform.
66
+ Copy supporting files, such as redistributables and generate Dockerfile for the build.
67
+
68
+ Args:
69
+ platform_parameters (PlatformParameters): platform parameters
70
+
71
+ Returns:
72
+ PlatformBuildResults: build results
73
+ """
74
+ self._copy_supporting_files(platform_parameters)
75
+ docker_file_path = self._write_dockerfile(platform_parameters)
76
+
77
+ return self._build_internal(docker_file_path, platform_parameters)
78
+
79
+ def _build_internal(
80
+ self, dockerfile: str, platform_parameters: PlatformParameters
81
+ ) -> PlatformBuildResults:
82
+ """Prepare parameters for Docker buildx build
83
+
84
+ Args:
85
+ dockerfile (str): Path to Dockerfile to be built
86
+ platform_parameters (PlatformParameters): platform parameters
87
+
88
+ Returns:
89
+ PlatformBuildResults: build results
90
+ """
91
+ self.print_build_info(platform_parameters)
92
+ builder = create_and_get_builder(Constants.LOCAL_BUILDX_BUILDER_NAME)
93
+
94
+ build_result = PlatformBuildResults(platform_parameters)
95
+
96
+ cache_to = {"type": "local", "dest": self._build_parameters.build_cache}
97
+ cache_from = [{"type": "local", "src": self._build_parameters.build_cache}]
98
+ if platform_parameters.base_image is not None:
99
+ cache_from.append(
100
+ {"type": "registry", "ref": platform_parameters.base_image}
101
+ )
102
+ if platform_parameters.build_image is not None:
103
+ cache_from.append(
104
+ {"type": "registry", "ref": platform_parameters.build_image}
105
+ )
106
+
107
+ builds = {
108
+ "builder": builder,
109
+ "cache": not self._build_parameters.no_cache,
110
+ "cache_from": None if self._build_parameters.no_cache else cache_from,
111
+ "cache_to": None if self._build_parameters.no_cache else cache_to,
112
+ "context_path": self._temp_dir,
113
+ "file": dockerfile,
114
+ "platforms": [platform_parameters.docker_arch],
115
+ "progress": "plain" if self._logger.root.level == logging.DEBUG else "auto",
116
+ "pull": True,
117
+ "tags": [platform_parameters.tag],
118
+ }
119
+
120
+ export_to_tar_ball = False
121
+ if self._build_parameters.tarball_output is not None:
122
+ build_result.tarball_filenaem = str(
123
+ self._build_parameters.tarball_output
124
+ / f"{platform_parameters.tag}{Constants.TARBALL_FILE_EXTENSION}"
125
+ ).replace(":", "-")
126
+
127
+ # Make result image available on 'docker image' only if arch matches
128
+ if platform_parameters.same_arch_as_system:
129
+ builds["load"] = True
130
+ build_result.docker_tag = platform_parameters.tag
131
+ export_to_tar_ball = self._build_parameters.tarball_output is not None
132
+ else:
133
+ if self._build_parameters.tarball_output is not None:
134
+ builds["output"] = {
135
+ # type=oci cannot be loaded by docker: https://github.com/docker/buildx/issues/59
136
+ "type": "docker",
137
+ "dest": build_result.tarball_filenaem,
138
+ }
139
+ else:
140
+ build_result.succeeded = False
141
+ build_result.error = (
142
+ "Skipped due to incompatible system architecture. "
143
+ "Use '--output' to write image to disk."
144
+ )
145
+ return build_result
146
+
147
+ builds["build_args"] = {
148
+ "UID": self._build_parameters.uid,
149
+ "GID": self._build_parameters.gid,
150
+ "UNAME": self._build_parameters.username,
151
+ "GPU_TYPE": platform_parameters.platform_config.value,
152
+ }
153
+
154
+ self._logger.debug(
155
+ f"Building Holoscan Application Package: tag={platform_parameters.tag}"
156
+ )
157
+
158
+ try:
159
+ build_docker_image(**builds)
160
+ build_result.succeeded = True
161
+ if export_to_tar_ball:
162
+ try:
163
+ self._logger.info(
164
+ f"Saving {platform_parameters.tag} to {build_result.tarball_filenaem}..."
165
+ )
166
+ docker_export_tarball(
167
+ build_result.tarball_filenaem, platform_parameters.tag
168
+ )
169
+ except Exception as ex:
170
+ build_result.error = f"Error saving tarball: {ex}"
171
+ build_result.succeeded = False
172
+ except Exception:
173
+ build_result.succeeded = False
174
+ build_result.error = (
175
+ "Error building image: see Docker output for additional details."
176
+ )
177
+
178
+ return build_result
179
+
180
+ def print_build_info(self, platform_parameters):
181
+ """Print build information for the platform."""
182
+ self._logger.info(
183
+ f"""
184
+ ===============================================================================
185
+ Building image for: {platform_parameters.platform.value}
186
+ Architecture: {platform_parameters.platform_arch.value}
187
+ Base Image: {platform_parameters.base_image}
188
+ Build Image: {platform_parameters.build_image if platform_parameters.build_image is not None else "N/A"}
189
+ Cache: {'Disabled' if self._build_parameters.no_cache else 'Enabled'}
190
+ Configuration: {platform_parameters.platform_config.value}
191
+ Holoscan SDK Package: {platform_parameters.holoscan_sdk_file if platform_parameters.holoscan_sdk_file is not None else "N/A"}
192
+ MONAI Deploy App SDK Package: {platform_parameters.monai_deploy_sdk_file if platform_parameters.monai_deploy_sdk_file is not None else "N/A"}
193
+ gRPC Health Probe: {platform_parameters.health_probe if platform_parameters.health_probe is not None else "N/A"}
194
+ SDK Version: {self._build_parameters.holoscan_sdk_version}
195
+ SDK: {self._build_parameters.sdk.value}
196
+ Tag: {platform_parameters.tag}
197
+ Included features/dependencies: {", ".join(self._build_parameters.includes) if self._build_parameters.includes else "N/A"}
198
+ """ # noqa: E501
199
+ )
200
+
201
+ def _write_dockerignore(self):
202
+ """Copy .dockerignore file to temporary location."""
203
+ # Write out .dockerignore file
204
+ dockerignore_source_file_path = (
205
+ Path(__file__).parent / "templates" / "dockerignore"
206
+ )
207
+ dockerignore_dest_file_path = os.path.join(self._temp_dir, ".dockerignore")
208
+ shutil.copyfile(dockerignore_source_file_path, dockerignore_dest_file_path)
209
+ return dockerignore_dest_file_path
210
+
211
+ def _copy_script(self):
212
+ """Copy HAP/MAP tools.sh script to temporary directory"""
213
+ # Copy the tools script
214
+ tools_script_file_path = Path(__file__).parent / "templates" / "tools.sh"
215
+ tools_script_dest_file_path = os.path.join(self._temp_dir, "tools")
216
+ shutil.copyfile(tools_script_file_path, tools_script_dest_file_path)
217
+ return tools_script_dest_file_path
218
+
219
+ def _write_dockerfile(self, platform_parameters: PlatformParameters):
220
+ """Write Dockerfile temporary location"""
221
+ docker_template_string = self._get_template(platform_parameters)
222
+ self._logger.debug(
223
+ f"""
224
+ ========== Begin Dockerfile ==========
225
+ {docker_template_string}
226
+ =========== End Dockerfile ===========
227
+ """
228
+ )
229
+
230
+ docker_file_path = os.path.join(self._temp_dir, DefaultValues.DOCKER_FILE_NAME)
231
+ with open(docker_file_path, "w") as docker_file:
232
+ docker_file.write(docker_template_string)
233
+
234
+ return os.path.abspath(docker_file_path)
235
+
236
+ def _copy_application(self):
237
+ """Copy application to temporary location"""
238
+ # Copy application files to temp directory (under 'app' folder)
239
+ target_application_path = Path(os.path.join(self._temp_dir, "app"))
240
+ if os.path.exists(target_application_path):
241
+ shutil.rmtree(target_application_path)
242
+
243
+ if not os.path.exists(self._build_parameters.application):
244
+ raise WrongApplicationPathError(
245
+ f'Directory "{self._build_parameters.application}" not found.'
246
+ )
247
+
248
+ if os.path.isfile(self._build_parameters.application):
249
+ shutil.copytree(
250
+ self._build_parameters.application.parent, target_application_path
251
+ )
252
+ else:
253
+ shutil.copytree(self._build_parameters.application, target_application_path)
254
+
255
+ target_config_file_path = Path(os.path.join(self._temp_dir, "app.config"))
256
+ shutil.copyfile(
257
+ self._build_parameters.app_config_file_path, target_config_file_path
258
+ )
259
+
260
+ def _copy_libs(self):
261
+ """
262
+ - Copy additional libraries to the temporary application directory.
263
+ - Stores all subdirectories from the copied libraries to the 'additional_lib_paths'
264
+ parameter that will be used to set the LD_LIBRARY_PATH or PYTHONPATH environment variable
265
+ in the Dockerfile.
266
+ """
267
+ if self._build_parameters.additional_libs is None:
268
+ return
269
+ target_libs_path = Path(os.path.join(self._temp_dir, "lib"))
270
+ if os.path.exists(target_libs_path):
271
+ shutil.rmtree(target_libs_path)
272
+
273
+ for lib_path in self._build_parameters.additional_libs:
274
+ self._logger.debug(
275
+ f"Copying additional libraries from {lib_path} to {target_libs_path}"
276
+ )
277
+ shutil.copytree(lib_path, target_libs_path, dirs_exist_ok=True)
278
+
279
+ subdirectories = [
280
+ os.path.join(
281
+ DefaultValues.HOLOSCAN_LIB_DIR,
282
+ os.path.join(root, subdir)
283
+ .replace(str(target_libs_path), "")
284
+ .lstrip("/"),
285
+ )
286
+ for root, dirs, _ in os.walk(target_libs_path)
287
+ for subdir in dirs
288
+ ]
289
+ self._build_parameters.additional_lib_paths = ":".join(subdirectories)
290
+
291
+ def _copy_model_files(self):
292
+ """Copy models to temporary location"""
293
+ if self._build_parameters.models:
294
+ target_models_root_path = os.path.join(self._temp_dir, "models")
295
+ os.makedirs(target_models_root_path, exist_ok=True)
296
+
297
+ for model in self._build_parameters.models:
298
+ target_model_path = os.path.join(target_models_root_path, model)
299
+ if self._build_parameters.models[model].is_dir():
300
+ shutil.copytree(
301
+ self._build_parameters.models[model], target_model_path
302
+ )
303
+ elif self._build_parameters.models[model].is_file():
304
+ os.makedirs(target_model_path, exist_ok=True)
305
+ target_model_path = os.path.join(
306
+ target_model_path, self._build_parameters.models[model].name
307
+ )
308
+ shutil.copy(self._build_parameters.models[model], target_model_path)
309
+
310
+ def _copy_docs(self):
311
+ """Copy user documentations to temporary location"""
312
+ if self._build_parameters.docs is not None:
313
+ target_path = os.path.join(self._temp_dir, "docs")
314
+ shutil.copytree(self._build_parameters.docs, target_path)
315
+
316
+ def _get_template(self, platform_parameters: PlatformParameters):
317
+ """Generate Dockerfile using Jinja2 engine"""
318
+ jinja_env = Environment(
319
+ loader=FileSystemLoader(Path(__file__).parent / "templates"),
320
+ undefined=StrictUndefined,
321
+ trim_blocks=True,
322
+ lstrip_blocks=True,
323
+ autoescape=True,
324
+ )
325
+ self._logger.debug(
326
+ f"""
327
+ ========== Begin Build Parameters ==========
328
+ {pprint.pformat(self._build_parameters.to_jinja)}
329
+ =========== End Build Parameters ===========
330
+ """
331
+ )
332
+ self._logger.debug(
333
+ f"""
334
+ ========== Begin Platform Parameters ==========
335
+ {pprint.pformat(platform_parameters.to_jinja)}
336
+ =========== End Platform Parameters ===========
337
+ """
338
+ )
339
+
340
+ jinja_template = jinja_env.get_template("Dockerfile.jinja2")
341
+ return jinja_template.render(
342
+ {
343
+ **self._build_parameters.to_jinja,
344
+ **platform_parameters.to_jinja,
345
+ }
346
+ )
347
+
348
+ def _copy_supporting_files(self, platform_parameters: PlatformParameters):
349
+ """Abstract base function to copy supporting files"""
350
+ return NotImplemented
351
+
352
+ def __init_subclass__(cls):
353
+ if cls._copy_supporting_files is BuilderBase._copy_supporting_files:
354
+ raise NotImplementedError(
355
+ "{cls} has not overwritten method {_copy_supporting_files}!"
356
+ )
357
+
358
+
359
+ class PythonAppBuilder(BuilderBase):
360
+ """A subclass of BuilderBase for Python-based applications.
361
+ Copioes PyPI package and requirement.txt file
362
+ """
363
+
364
+ def __init__(
365
+ self,
366
+ build_parameters: PackageBuildParameters,
367
+ temp_dir: str,
368
+ ) -> None:
369
+ BuilderBase.__init__(self, build_parameters, temp_dir)
370
+
371
+ def _copy_supporting_files(self, platform_parameters: PlatformParameters):
372
+ self._copy_sdk_file(platform_parameters.holoscan_sdk_file)
373
+ self._copy_sdk_file(platform_parameters.monai_deploy_sdk_file)
374
+ self._copy_pip_requirements()
375
+
376
+ def _copy_pip_requirements(self):
377
+ pip_folder = os.path.join(self._temp_dir, "pip")
378
+ os.makedirs(pip_folder, exist_ok=True)
379
+ pip_requirements_path = os.path.join(pip_folder, "requirements.txt")
380
+ with open(pip_requirements_path, "w") as requirements_file:
381
+ # Use local requirements.txt packages if provided, otherwise use sdk provided packages
382
+ if self._build_parameters.requirements_file_path is not None:
383
+ with open(self._build_parameters.requirements_file_path) as lr:
384
+ for line in lr:
385
+ requirements_file.write(line)
386
+ requirements_file.writelines("\n")
387
+
388
+ if self._build_parameters.pip_packages:
389
+ requirements_file.writelines(
390
+ "\n".join(self._build_parameters.pip_packages)
391
+ )
392
+
393
+ def _copy_sdk_file(self, sdk_file: Optional[Path]):
394
+ if sdk_file is not None and os.path.isfile(sdk_file):
395
+ dest = os.path.join(self._temp_dir, sdk_file.name)
396
+ if os.path.exists(dest):
397
+ os.remove(dest)
398
+ shutil.copyfile(sdk_file, dest)
399
+
400
+
401
+ class CppAppBuilder(BuilderBase):
402
+ """A subclass of BuilderBase for C++ applications.
403
+ Copies Debian.
404
+ """
405
+
406
+ def __init__(
407
+ self,
408
+ build_parameters: PackageBuildParameters,
409
+ temp_dir: str,
410
+ ) -> None:
411
+ BuilderBase.__init__(self, build_parameters, temp_dir)
412
+
413
+ def _copy_supporting_files(self, platform_parameters: PlatformParameters):
414
+ """Copies the SDK file to the temporary directory"""
415
+ if platform_parameters.holoscan_sdk_file is not None and os.path.isfile(
416
+ platform_parameters.holoscan_sdk_file
417
+ ):
418
+ dest = os.path.join(
419
+ self._temp_dir, platform_parameters.holoscan_sdk_file.name
420
+ )
421
+ if os.path.exists(dest):
422
+ os.remove(dest)
423
+ shutil.copyfile(
424
+ platform_parameters.holoscan_sdk_file,
425
+ dest,
426
+ )